# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2008-2010 Craig J. Anderson # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # $Id: Descendant.py .... ander882 $ """Reports/Graphical Reports/Tree_Base""" #------------------------------------------------------------------------ # # python modules # #------------------------------------------------------------------------ from gen.ggettext import sgettext as _ from gen.plug.report import utils as ReportUtils from gen.display.name import displayer as name_displayer from libsubstkeyword import SubstKeywords PT2CM = ReportUtils.pt2cm #------------------------------------------------------------------------ # # Class Calc_Lines # #------------------------------------------------------------------------ class CalcLines(object): """ wrapper for libsubstkeyword and added functionality for replacements. Receive: Individual and family handle, and display format [string] return: [Text] ready for a box. """ def __init__(self, dbase, repl): self.database = dbase self.display_repl = repl #self.default_string = default_str def calc_lines(self, _indi_handle, _fams_handle, workinglines): """ In this pass we will: 1. make our text and do our replacements 2. remove any extra (unwanted) lines with the compres option """ #################### #1.1 Get our line information here subst = SubstKeywords(self.database, _indi_handle, _fams_handle) lines = subst.replace_and_clean(workinglines) #################### #1.2 do our replacements lns = [] for line in lines: for pair in self.display_repl: if pair.count("/") == 1: repl = pair.split("/", 1) line = line.replace(repl[0], repl[1]) lns.append(line) return lns #------------------------------------------------------------------------ # # Class Canvas/Pages # #------------------------------------------------------------------------ class Page(object): """ This class is a printable page. Offsets from the canvas, Page numbers boxes and lines """ def __init__(self, doc, canvas): #parts from canvas self.doc = doc self.canvas = canvas #parts about the page self.page_x_offset = 0 self.page_y_offset = 0 self.x_page_num = 0 self.y_page_num = 0 self.boxes = [] #All object must derive from BoxBase self.lines = [] #All must derive form Linebase self.note = None def is_blank(self): """ Am I a blank page? Notes and Titles are boxes too """ return self.boxes == [] and self.lines == [] def add_box(self, box): """ The box must derive from class Box_Base(Object): """ self.boxes.append(box) box.page = self def add_line(self, line): """ Add a line onto this page """ self.lines.append(line) def draw_border(self, line_name): if self.y_page_num == 0: self.doc.draw_line(line_name, 0, 0, self.doc.get_usable_width(), 0) if self.x_page_num == 0: self.doc.draw_line(line_name, 0, 0, 0, self.doc.get_usable_height()) if self.y_page_num == self.canvas.y_pages-1: self.doc.draw_line(line_name, 0, self.doc.get_usable_height(), self.doc.get_usable_width(), self.doc.get_usable_height()) if self.x_page_num == self.canvas.x_pages-1: self.doc.draw_line(line_name, self.doc.get_usable_width(), 0, self.doc.get_usable_width(), self.doc.get_usable_height()) def display(self): """ Display all boxes and lines that are on this page """ for box in self.boxes: box.display() for line in self.lines: line.display(self) class Canvas(Page): """ The Canvas is two things. The all in one canvas. a canvas is a page of unlimited size a group of pages. each page is set is size and shows only a part of what is on the entire canvas """ def __init__(self, doc): Page.__init__(self, doc, self) self.doc = doc self.report_opts = None #How many pages are there in the report. one more than real. self.x_pages = 1 self.y_pages = 1 self.__pages = {(0, 0): self} #set page 0,0 to me. self.__fonts = {} #keep a list of fonts so we don't have to lookup. self.title = None self.note = None def __new_page(self, x_page, y_page, x_offset, y_offset): """ Make a new page. This will only happen if we are paginating (making new pages to hold parts of the canvas) """ if x_page >= self.x_pages: self.x_pages = x_page + 1 new_page = Page(self.doc, self) new_page.x_page_num = x_page new_page.y_page_num = y_page new_page.page_x_offset = x_offset new_page.page_y_offset = y_offset self.__pages[x_page, y_page] = new_page return new_page def sort_boxes_on_y_cm(self): """ sorts the list of boxes on the canvas by .y_cm (top down) """ self.boxes.sort( key=lambda box: box.y_cm) def add_title(self, title): """ The title must derive from class TitleBox(BoxBase): """ self.title = title def add_note(self, note): """ The note must derive from class NoteBox(BoxBase, NoteType) """ self.note = note self.set_box_height_width(self.note) def __get_font(self, box): """ returns the font used by a box. makes a list of all seen fonts to be faster. If a new is found, run through the process to get it """ if not self.__fonts.has_key(box.boxstr): style_sheet = self.doc.get_style_sheet() style_name = style_sheet.get_draw_style(box.boxstr) style_name = style_name.get_paragraph_style() self.__fonts[box.boxstr] = \ style_sheet.get_paragraph_style(style_name).get_font() return self.__fonts[box.boxstr] def get_report_height_width(self): """ returns the (max width, max height) of the report This does not take into account any shadows """ max_width = 0 max_height = 0 for box in self.boxes: tmp = box.x_cm + box.width + box.shadow if tmp > max_width: max_width = tmp tmp = box.y_cm + box.height + box.shadow if tmp > max_height: max_height = tmp return (max_width, max_height) def __scale_canvas(self, scale_amount): """ scales everything up/down depending upon scale_amount """ self.doc.report_opts.scale_everything(scale_amount) self.title.scale(scale_amount) if self.note is not None: self.note.scale(scale_amount) #scale down everyone! for box in self.boxes: box.scale(scale_amount) def set_box_height_width(self, box): """ Sets the .width .height and .shadow of a box. """ if box.boxstr == "None": box.height = box.width = 0 return font = self.__get_font(box) ##################### #Get the width for line in box.text: width = self.doc.string_width(font, line) width = PT2CM(width) if width > box.width: box.width = width ##################### #Get the height height = len(box.text) * font.get_size() * 1.5 height += 1.0/2.0 * font.get_size() #funny number(s) based upon font. box.height = PT2CM(height) style_sheet = self.doc.get_style_sheet() style = style_sheet.get_draw_style(box.boxstr) if style.get_shadow(): box.shadow = style.get_shadow_space() def page_iter_gen(self, incblank): """ generate the pages of the report. do so in a left to right up down approach. incblank asks to include blank pages """ blank = Page(self.doc, self) for y_p in range(self.y_pages): for x_p in range(self.x_pages): if self.__pages.has_key((x_p, y_p)): yield self.__pages[(x_p, y_p)] else: if incblank: blank.x_page_num = x_p blank.y_page_num = y_p yield blank def __add_box_to_page(self, x_page, y_page, x_offset, y_offset, box): """ adds a box to a page. If the page is not there, make it first """ if not self.__pages.has_key((x_page, y_page)): #Add the new page into the dictionary self.__new_page(x_page, y_page, x_offset, y_offset) #Add the box into the page self.__pages[x_page, self.y_pages-1].add_box(box) def scale_report(self, one_page, scale_to_width, scale_to_height): """ We have a report in its full size (on the canvas and pages to print on. scale one or both as needed/desired. - one_page, boolean. Whether to make the page(or parts of) the size of the report - scale_to_width, boolean. Scale the report width to the page size? - scale_to_height, boolean. Scale the report height to page size? """ if scale_to_width or scale_to_height: max_width, max_height = self.canvas.get_report_height_width() max_width += self.doc.report_opts.littleoffset max_height += self.doc.report_opts.littleoffset """ calc - Calculate the scale amount (if any). <1 everything is smaller to fit on the page 1 == no scaling >1 make everything bigger to fill out the page """ scale = 1 scaled_report_to = None ##################### #scale the report option - width if scale_to_width: #Check the width of the title title_width = self.title.width title_width += self.doc.report_opts.littleoffset * 2 max_width = max(title_width, max_width) #This will be our base amount and #then we will decrease only as needed from here. scale = self.doc.get_usable_width() / max_width scaled_report_to = "width" ##################### #scale the report option - height if scale_to_height: tmp = self.doc.get_usable_height() / max_height if not scale_to_width or tmp < scale: scale = tmp scaled_report_to = "height" #Now I have the scale amount if scale != 1: #scale everything on the canvas self.__scale_canvas(scale) ##################### #Scale the page option if one_page: #user wants PAGE to be the size of the report. size = self.doc.paper.get_size() max_width, max_height = \ self.canvas.get_report_height_width() if scaled_report_to != "width": #calculate the width of the report max_width += self.doc.report_opts.littleoffset max_width += self.doc.paper.get_left_margin() max_width += self.doc.paper.get_right_margin() #calculate the width of the title title_width = self.canvas.title.width title_width += self.doc.paper.get_left_margin() title_width += self.doc.paper.get_right_margin() title_width += self.doc.report_opts.littleoffset max_width = max(title_width, max_width) size.set_width(max_width) if scaled_report_to != "height": #calculate the height of the report max_height += self.doc.paper.get_top_margin() max_height += self.doc.paper.get_bottom_margin() max_height += self.doc.report_opts.littleoffset size.set_height(max_height) return scale def __paginate_x_offsets(self, colsperpage): """ Go through the boxes and get the x page offsets """ #fix soon. should not use .level liloffset = self.doc.report_opts.littleoffset x_page_offsets = {0:0} #change me to [] ??? for box in self.boxes: x_index = box.level[0] x_page = x_index / colsperpage if x_page not in x_page_offsets and x_index % colsperpage == 0: x_page_offsets[x_page] = box.x_cm - liloffset if x_page >= self.x_pages: self.x_pages = x_page+1 return x_page_offsets def __paginate_y_pages(self, colsperpage, x_page_offsets): """ Go through the boxes and put each one in a page note that the self.boxes needs to be sorted by .y_cm """ page_y_top = [0] page_y_height = [self.doc.get_usable_height()] liloffset = self.doc.report_opts.littleoffset for box in self.boxes: #check to see if this box cross over to the next (y) page height = box.y_cm + liloffset + box.height + box.shadow/2 if height > page_y_height[-1]: #we went off the end page_y_height.append(box.y_cm - liloffset + page_y_height[0]) page_y_top.append(box.y_cm - liloffset) self.y_pages = len(page_y_height) #Calculate my (x) page #fix soon. should not use .level x_page = box.level[0] / colsperpage self.__add_box_to_page(x_page, self.y_pages-1, x_page_offsets[x_page], page_y_top[self.y_pages-1], box) #if not self.__pages.has_key((x_page, self.y_pages-1)): # #Add the new page into the dictionary # self.__new_page(x_page, self.y_pages-1, # ) # ##Add the box into the page #self.__pages[x_page, self.y_pages-1].add_box(box) return page_y_top def __paginate_note(self, x_page_offsets, page_y_top): """ Put the note on first. it can be overwritten by other boxes but it CAN NOT overwrite a box. """ x_page, y_page = self.note.set_on_page(self) if not self.__pages.has_key((x_page, y_page)): #Add the new page into the dictionary self.__new_page(x_page, y_page, x_page_offsets[x_page], page_y_top[y_page]) #Add the box into the page self.__pages[x_page, y_page].boxes.insert(0, self.note) self.note.doc = self.doc self.note.page = self def __paginate_lines(self, x_page_offsets, page_y_top): """ Step three go through the lines and put each in page(s) """ for line in self.lines: pages = [] #if type(line.start) == type([]): pages = [] end = line.start + line.end #else: # end = [line.start] + line.end # pages = [] start_x_page = end[0].page.x_page_num start_y_page = end[0].page.y_page_num end_y_page = end[0].page.y_page_num for box in end: x_page = box.page.x_page_num y_page = box.page.y_page_num if (x_page, y_page) not in pages: if not self.__pages.has_key((x_page, y_page)): #Add the new page into the dictionary self.__new_page(x_page, y_page, x_page_offsets[x_page], page_y_top[y_page]) self.__pages[x_page, y_page].add_line(line) pages.append((x_page, y_page)) if y_page < start_y_page: start_y_page = y_page if y_page > end_y_page: end_y_page = y_page #if len(end) = 2 & end[0].y_page = 0 & end[1].y_page = 4 #the line will not print on y_pages 1,2,3. Fix that here. x_page = start_x_page for y_page in range(start_y_page, end_y_page+1): if (x_page, y_page) not in pages: if not self.__pages.has_key((x_page, y_page)): #Add the new page into the dictionary self.__new_page(x_page, y_page, x_page_offsets[x_page], page_y_top[y_page]) self.__pages[x_page, y_page].add_line(line) def __paginate_title(self, x_page_offsets): #step four work with the title if self.title.boxstr == "None": return #x_page_offsets[page] tells me the widths I can use if len(x_page_offsets) > 1: title_list = self.title.text.split(" ") title_font = self.__get_font(self.title) #space_width = PT2CM(self.doc.string_width(title_font," ")) list_title = [title_list.pop(0)] while len(title_list): tmp = list_title[-1] + " " + title_list[0] if PT2CM(self.doc.string_width(title_font, tmp)) > \ x_page_offsets[1]: list_title.append("") if list_title[-1] != "": list_title[-1] += " " list_title[-1] += title_list.pop(0) start_page = (len(x_page_offsets) - len(list_title)) / 2 for tmp in range(start_page): list_title.insert(0, "") list_title.append("") list_title.append("") #one extra for security. doesn't hurt. x_page = 0 for title in list_title: if title == "": x_page += 1 continue if not self.__pages.has_key((x_page, 0)): #Add the new page into the dictionary self.__new_page(x_page, 0, x_page_offsets[1], 0) title_part = TitleBox(self.doc, self.title.boxstr) title_part.text = list_title[x_page] title_part.width = x_page_offsets[1] #Add the box into the page self.__pages[x_page, 0].add_box(title_part) x_page = x_page + 1 else: self.title.width = self.doc.get_usable_width() self.__pages[0, 0].add_box(self.title) def __paginate(self, colsperpage): """ take the boxes on the canvas and put them into separate pages. The boxes need to be sorted by y_cm """ liloffset = self.doc.report_opts.littleoffset self.__pages = {} x_page_offsets = self.__paginate_x_offsets(colsperpage) page_y_top = self.__paginate_y_pages(colsperpage, x_page_offsets) if self.note is not None: self.__paginate_note(x_page_offsets, page_y_top) self.__paginate_lines(x_page_offsets, page_y_top) self.__paginate_title(x_page_offsets) def paginate(self, colsperpage, one_page_report): """ self.boxes must be sorted by box.y_cm for this to work. """ if one_page_report: #self.canvas.add_box(self.canvas.title) title_part = TitleBox(self.doc, self.title.boxstr) title_part.text = self.title.text title_part.width = self.doc.get_usable_width() self.add_box(title_part) if self.note is not None: self.note.set_on_page(self) self.boxes.insert(0, self.note) self.note.doc = self.doc self.note.page = self else: self.__paginate(colsperpage) #------------------------------------------------------------------------ # # Class Box_Base # #------------------------------------------------------------------------ class BoxBase(object): """ boxes are always in/on a Page Needed to print are: boxstr, text, x_cm, y_cm, width, height """ def __init__(self): self.page = None #'None' will cause an error. Sub-classes will init self.boxstr = "None" self.text = "" self.level = (0,) #which column/level am I in? int zero based. self.x_cm = 0.0 self.y_cm = 0.0 self.width = 0.0 self.height = 0.0 self.shadow = 0.0 def scale(self, scale_amount): """ Scale the amounts """ self.x_cm *= scale_amount self.y_cm *= scale_amount self.width *= scale_amount self.height *= scale_amount self.shadow *= scale_amount def display(self): """ display the box accounting for page x, y offsets Ignore any box with 'None' is boxstr """ if self.boxstr != "None": text = '\n'.join(self.text) xbegin = self.x_cm - self.page.page_x_offset ybegin = self.y_cm - self.page.page_y_offset self.page.doc.draw_box(self.boxstr, text, xbegin, ybegin, self.width, self.height) class TitleBox(BoxBase): """ Holds information about the Title that will print on a page """ def __init__(self, doc, boxstr): """ initalize the title box """ BoxBase.__init__(self) self.doc = doc self.boxstr = boxstr if boxstr == "None": return self.cm_y = self.doc.report_opts.littleoffset style_sheet = self.doc.get_style_sheet() style_name = style_sheet.get_draw_style(self.boxstr) style_name = style_name.get_paragraph_style() self.font = style_sheet.get_paragraph_style(style_name).get_font() def set_box_height_width(self): if self.boxstr == "None": return #fix me. width should be the printable area self.width = PT2CM(self.doc.string_width(self.font, self.text)) self.height = PT2CM(self.font.get_size() * 1.2) def _get_names(self, persons): """ A helper function that receives a list of persons and returns their names in a list """ tmp = [] for person in persons: tmp.append(name_displayer.display(person)) return tmp def display(self): """ display the title box. """ if self.page.y_page_num != 0 or self.boxstr == "None": return if self.text != "": self.doc.center_text(self.boxstr, self.text, self.width/2, self.y_cm) class PageNumberBox(BoxBase): """ Calculates information about the page numbers that will print on a page do not put in a value for PageNumberBox.text. this will be calculated for each page """ def __init__(self, doc, boxstr): """ initalize the page number box """ BoxBase.__init__(self) self.doc = doc self.boxstr = boxstr def __calc_position(self, page): """ calculate where I am to print on the page(s) """ self.text = "(%d,%d)" style_sheet = self.doc.get_style_sheet() style_name = style_sheet.get_draw_style(self.boxstr) style_name = style_name.get_paragraph_style() font = style_sheet.get_paragraph_style(style_name).get_font() #calcualate how much space is needed if page.canvas.x_pages > 10: tmp = "00" else: tmp = "0" if page.canvas.y_pages > 10: tmp += "00" else: tmp += "0" width = self.doc.string_width(font, '(,)'+tmp) width = PT2CM(width) self.width = width height = font.get_size() * 1.4 height += 0.5/2.0 * font.get_size() #funny number(s) based upon font. self.height = PT2CM(height) self.x_cm = self.doc.get_usable_width() - self.width self.y_cm = self.doc.get_usable_height() - self.height def display(self, page): """ If this is the first time I am ran, get my position then display the page number """ if self.text == "": self.__calc_position(page) self.doc.draw_text(self.boxstr, self.text % (page.x_page_num+1,page.y_page_num+1), self.x_cm, self.y_cm) class NoteType(object): """ Provide the different options (corners) to place the note """ TOPLEFT = 0 TOPRIGHT = 1 BOTTOMLEFT = 2 BOTTOMRIGHT = 3 _DEFAULT = BOTTOMRIGHT _DATAMAP = [ (TOPLEFT, _("Top Left"), "Top Left"), (TOPRIGHT, _("Top Right"), "Top Right"), (BOTTOMLEFT, _("Bottom Left"), "Bottom Left"), (BOTTOMRIGHT, _("Bottom Right"), "Bottom Right"), ] def __init__(self, value, exclude=None): """ initalize GrampsType """ self.value = value self.exclude = exclude #GrampsType.__init__(self, value) def note_locals(self, start=0): """ generates an int of all the options """ for tuple in self._DATAMAP: if tuple[0] != self.exclude: yield tuple[0], tuple[1] class NoteBox(BoxBase, NoteType): """ Box that will hold the note to display on the page """ def __init__(self, doc, boxstr, locale, exclude=None): """ initalize the NoteBox """ BoxBase.__init__(self) NoteType.__init__(self, locale, exclude) self.doc = doc self.boxstr = boxstr def set_on_page(self, canvas): """ set the x_cm and y_cm given self.doc, leloffset, and title_height """ liloffset = self.doc.report_opts.littleoffset #left or right side if self.value == NoteType.BOTTOMLEFT or \ self.value == NoteType.TOPLEFT: self.x_cm = liloffset else: self.x_cm = self.doc.get_usable_width() - self.width - liloffset #top or bottom if self.value == NoteType.TOPRIGHT or \ self.value == NoteType.TOPLEFT: self.y_cm = canvas.title.height + liloffset*2 else: self.y_cm = self.doc.get_usable_height() - self.height - liloffset """ helper function for canvas.paginate(). return the (x, y) page I want to print on """ if self.value == NoteType.TOPLEFT: return (0, 0) elif self.value == NoteType.TOPRIGHT: return (canvas.x_pages-1, 0) elif self.value == NoteType.BOTTOMLEFT: return (0, canvas.y_pages-1) elif self.value == NoteType.BOTTOMRIGHT: return (canvas.x_pages-1, canvas.y_pages-1) def display(self): """ position the box and display """ title = self.page.canvas.title title_height = 0 if title is not None: title_height = title.height text = '\n'.join(self.text) self.doc.draw_box(self.boxstr, text, self.x_cm, self.y_cm, self.width, self.height) #------------------------------------------------------------------------ # # Class Line_base # #------------------------------------------------------------------------ class LineBase(object): """ A simple line class. self.start is the box that we are drawing a line from self.end are the boxes that we are drawing lines to. """ def __init__(self, start): self.linestr = "None" self.start = [start] self.end = [] def add_to(self, person): """ add destination boxes to draw this line to """ self.end.append(person) def display(self, page): """ display the line. left to right line. one start, multiple end. page will tell us what parts of the line we can print """ if self.end == []: return # y_cm and x_cm start points - take into account page offsets #yme = self.start.y_cm + self.start.height/2 - page.page_y_offset #if type(self.start) != type([]): # self.start = [self.start] start = self.start[0] doc = start.page.doc xbegin = start.x_cm + start.width - page.page_x_offset # out 3/4 of the way and x_cm end point(s) x34 = xbegin + (doc.report_opts.col_width * 3/4) xend = xbegin + doc.report_opts.col_width if x34 > 0: # > 0 tell us we are printing on this page. usable_height = doc.get_usable_height() #1 - Line from start box out for box in self.start: yme = box.y_cm + box.height/2 - page.page_y_offset if box.page.y_page_num == page.y_page_num: # and 0 < yme < usable_height and \ doc.draw_line(self.linestr, xbegin, yme, x34, yme) #2 - veritcal line mid = [] for box in self.start + self.end: tmp = box.y_cm + box.height/2 mid.append(tmp) mid.sort() mid = [mid[0]-page.page_y_offset, mid[-1]-page.page_y_offset] if mid[0] < 0: mid[0] = 0 if mid[1] > usable_height: mid[1] = usable_height #draw the connecting vertical line. doc.draw_line(self.linestr, x34, mid[0], x34, mid[1]) else: x34 = 0 #3 - horizontal line(s) for box in self.end: if box.page.y_page_num == page.y_page_num: yme = box.y_cm + box.height/2 - box.page.page_y_offset doc.draw_line(self.linestr, x34, yme, xend, yme) #------------------------------------------------------------------------ # # Class report_options # #------------------------------------------------------------------------ class ReportOptions(object): """ A simple class to hold various report information Calculates the gap between persons, the column width, for lines, the left hand spacing for spouses (Descendant report only) """ def __init__(self, doc, normal_font): """ initalize various report variables that are used """ self.box_pgap = PT2CM(1.25*normal_font.get_size()) #gap between persons self.box_mgap = self.box_pgap /2 #gap between marriage information self.box_shadow = PT2CM(9) #size of normal text self.spouse_offset = PT2CM(doc.string_width(normal_font, "0")) self.col_width = PT2CM(doc.string_width(normal_font, "(000,0)")) self.littleoffset = PT2CM(1) #Things that will get added later self.max_box_width = 0 self.max_box_height = 0 self.scale = 1 def scale_everything(self, amount): """ Scale the amounts that are needed to generate a report """ self.scale = amount self.col_width *= amount self.littleoffset *= amount self.max_box_width *= amount #box_width self.spouse_offset *= amount self.box_shadow *= amount #===================================== #"And Jesus said unto them ... , "If ye have faith as a grain of mustard #seed, ye shall say unto this mountain, Remove hence to younder place; and #it shall remove; and nothing shall be impossible to you." #Romans 1:17