#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2006 Donald N. Allingham
# Copyright (C) 2008 Willem van Engen
#
# 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
#
# History:
# 20080105 Encode data properly for xml.
# 20080103 Willem van Engen's initial release. Much rewritten, general
# filter support, address without lon/lat, customizable title.
# Family events are missing though.
# 2006 Donald Allingham's release
"""
Contains the interface to allow to write place data.
One backend for writing is supported: the KML file format used by google earth
info: bm at cage dot UGent dot be, dev-gramps@willem.engen.nl
"""
#-------------------------------------------------------------------------
#
# load standard python libraries
#
#-------------------------------------------------------------------------
import time
import os
import codecs
from gettext import gettext as _
from xml.sax import saxutils
import Utils
#-------------------------------------------------------------------------
#
# load GRAMPS libraries
#
#-------------------------------------------------------------------------
import RelLib
#------------------------------------------------------------------------
#
# Set up logging
#
#------------------------------------------------------------------------
import logging
log = logging.getLogger(".WriteGreographicData")
#-------------------------------------------------------------------------
#
# backend: KML writer for the actual writing of geographic data
#
#-------------------------------------------------------------------------
class KmlWriter:
"""
Writes output to a KML file.
"""
def __init__(self, title, placemarks=(), placestyles={}):
"""
Initializes, but does not write, a KML file.
@param title: title of the KML group
@param placemarks: a list of placemarks to write. Each placemark
is a dictionary, contaning the items:
(required:) title, shortdesc, longdesc_html, style;
(optional:) date, dateend, address, lon, lat
@param placestyles: different styles to show data in kml.
A dictionary with key stylename, data style.
TODO: Currently only the keys are used, indicating image files.
"""
self.title = title
self.styles = placestyles
self.places = placemarks
def write(self,filename):
"""
Write the data to the specified file.
"""
base = os.path.dirname(filename)
if os.path.isdir(base):
if not os.access(base,os.W_OK) or not os.access(base,os.R_OK):
ErrorDialog(_('Failure writing %s') % filename,
_("The file cannot be saved because you do "
"not have permission to write to the directory. "
"Please make sure you have write access to the "
"directory and try again."))
return 0
if os.path.exists(filename):
if not os.access(filename,os.W_OK):
ErrorDialog(_('Failure writing %s') % filename,
_("The file cannot be saved because you do "
"not have permission to write to the file. "
"Please make sure you have write access to the "
"file and try again."))
return 0
self.fileroot = os.path.dirname(filename)
try:
g = open(filename,"w")
except IOError,msg:
ErrorDialog(_('Failure writing %s') % filename,msg)
return 0
self.g = codecs.getwriter("utf8")(g)
self._write_kml_data()
g.close()
return 1
def write_handle(self,handle):
"""
Write the data to the specified file handle.
"""
g = handle
self.g = codecs.getwriter("utf8")(g)
self._write_kml_data()
g.close()
return 1
def _write_kml_data(self):
"""
Write the already opened KML file.
"""
date = time.localtime(time.time())
place_len = len(self.places)
self.g.write('\n')
self.g.write('\n')
# some info
self.g.write('\n'%
(date[0],date[1],date[2]) )
# start the document
self.g.write(" \n")
# set the name of the document
self.g.write(" %s" %(self.title))
# set the default styles used; use unique values from eventstyle lists
for style in self.styles.keys():
self._write_style('gramps'+style, 'event'+style+'.png')
# set all placemarks
for placemark in self.places:
self._write_placemark(placemark)
self.g.write("\n")
self.g.write(" \n")
self.g.write(" \n")
def _write_style(self,id,icon):
"""
Writes a style to the KML file
@param id: id of the style
@type id: string
@param icon: icon filename associated with this style, located
in /KML/. Specify None if no icon is present.
@type icon: string
"""
# XXX according to the KML 2.1 spec, Icon should have a single href
# child element. I haven't been able though to reference local
# images in this way though.
self.g.write("""
""")
def _write_placemark(self,placemark):
"""
Writes a placemark to the KML file
@param placemark: placemark properties
"""
# write KML placemark
self.g.write("""
%(name)s
1
1
%(snip)s
%(desc)s
gramps%(style)s""" % { \
'name': saxutils.escape(placemark['title']), \
'snip': saxutils.escape(placemark['shortdesc']), \
'desc': placemark['longdesc_html'], \
'style': saxutils.escape(placemark['style']) } )
if placemark.has_key('date') and placemark['date']:
if placemark.has_key('dateend') and placemark['dateend']:
# timespan
self.g.write("""
%(begin)s
%(end)s
""" % { \
'begin': saxutils.escape(placemark['date']), \
'end': saxutils.escape(placemark['dateend']) } )
else:
# timestamp
self.g.write("""
%(date)s""" % { \
'date': saxutils.escape(placemark['date']) } )
if placemark.has_key('address') and placemark['address']:
self.g.write("""
%(address)s""" % { \
'address': saxutils.escape(placemark['address']) })
if placemark.has_key('lon') and placemark.has_key('lat') and \
placemark['lon'] and placemark['lat']:
self.g.write("""
%(lon).14f,%(lat).14f
""" % { \
'lon': placemark['lon'], \
'lat': placemark['lat'] } )
self.g.write("""
""")
#------------------------------------------------------------------------
#
# GNOME/gtk
#
#------------------------------------------------------------------------
import gtk
#-------------------------------------------------------------------------
#
# load GRAMPS libraries
#
#-------------------------------------------------------------------------
import const
from QuestionDialog import ErrorDialog
from PluginUtils import register_report
from ReportBase import CATEGORY_CODE, MODE_GUI, Report, ReportOptions
from ReportBase._ReportDialog import ReportDialog
from NameDisplay import displayer as _nd
from DateHandler import displayer as _dd
#import DateHandler
import PlaceUtils
from Filters import GenericFilter, Rules
#------------------------------------------------------------------------
#
# Report class
#
#------------------------------------------------------------------------
class GeographicData:
def __init__(self,database,person,options_class):
self.database = database
self.start_person = person
options = options_class.handler.options_dict
#------------------------------------------------------------------------
#
# Constant options items
#
#------------------------------------------------------------------------
class _options:
# internal ID, english option name (for cli), localized option name (for gui)
formats = (
("kml", "Google Earth KML", _("Google Earth KML"), "application/kml"),
)
#check if googleearth is installed.
# --> problem, googleearth is normally installed locally, not in the PATH variable... TODO
if os.sys.platform == "win32":
_gge_found = Utils.search_for("googleearth.exe")
else:
_gge_found = Utils.search_for("googleearth")
#------------------------------------------------------------------------
#
# Options class
#
#------------------------------------------------------------------------
class GeographicDataOptions(ReportOptions):
"""
Defines options and provides handling interface.
"""
def __init__(self,name,person_id=None):
ReportOptions.__init__(self,name,person_id)
def set_new_options(self):
# Options specific for this report
self.options_dict = {
'unknownplace' : 0,
'unknowndate' : 1,
'filter' : 0,
'titleformat' : 1,
}
self.options_help = {
'unknownplace': ("=0/1","Whether to show events with unknown location.",
["Show events with known location","Show events with or without a location"],
False),
'unknowndate': ("=0/1","Whether to show events with no date specified.",
["Show only events with known date", "Show events with or without date"],
False),
'titleformat': ("=str","What to use as placemark titles.",
self._get_report_nameformatters_sample(),
False),
}
def make_doc_menu(self,dialog,active=None):
pass
def add_list(self, options, default):
"returns combobox of given options and default value"
box = gtk.ComboBox()
store = gtk.ListStore(str)
box.set_model(store)
cell = gtk.CellRendererText()
box.pack_start(cell,True)
box.add_attribute(cell,'text',0)
for i in range(len(options)):
store.append(row=[options[i]])
if i == default:
box.set_active(i)
return box
def add_user_options(self,dialog):
# Content of the options tab
msg = _("Placemark title format")
self.title_format = self.add_list(
self._get_report_nameformatters_sample(),
self.options_dict['titleformat'])
dialog.add_option(None,
self.title_format,
_("Placemark title format"))
msg = _("Include Events with no place given")
self.include_place_unknown = gtk.CheckButton(msg)
self.include_place_unknown.set_active(self.options_dict['unknownplace'])
dialog.add_option(None,
self.include_place_unknown,
_("Include events which have no place specified."))
msg = _("Include Events with no date given")
self.include_date_unknown = gtk.CheckButton(msg)
self.include_date_unknown.set_active(self.options_dict['unknowndate'])
dialog.add_option(None,
self.include_date_unknown,
_("Include events which have no date attached."))
def parse_user_options(self,dialog):
self.options_dict['titleformat'] = \
int(self.title_format.get_active())
self.options_dict['unknownplace'] = \
int(self.include_place_unknown.get_active())
self.options_dict['unknowndate'] = \
int(self.include_date_unknown.get_active())
def get_report_filters(self,person):
"""Set up the list of possible content filters."""
# thank you, IndivComplete.py :)
if person:
name = _nd.display(person)
gramps_id = person.get_gramps_id()
else:
name = 'PERSON'
gramps_id = ''
filt_id = GenericFilter()
filt_id.set_name(name)
filt_id.add_rule(Rules.Person.HasIdOf([gramps_id]))
all = GenericFilter()
all.set_name(_("Entire Database"))
all.add_rule(Rules.Person.Everyone([]))
des = GenericFilter()
des.set_name(_("Descendants of %s") % name)
des.add_rule(Rules.Person.IsDescendantOf([gramps_id,1]))
ans = GenericFilter()
ans.set_name(_("Ancestors of %s") % name)
ans.add_rule(Rules.Person.IsAncestorOf([gramps_id,1]))
com = GenericFilter()
com.set_name(_("People with common ancestor with %s") % name)
com.add_rule(Rules.Person.HasCommonAncestorWith([gramps_id]))
the_filters = [filt_id,all,des,ans,com]
from Filters import CustomFilters
the_filters.extend(CustomFilters.get_filters('Person'))
return the_filters
def get_report_nameformatters(self):
""" Returns a list of functions that convert a name into a string """
# Note that the code below uses string[:1] instead of string[0] to
# obtain a name's first letter. This avoids an IndexError and
# returns the empty string when string has length zero.
return [
# Doe, John Chris
lambda name: \
' '.join(filter(None,
[name.get_surname_prefix(), name.get_surname()])) \
+ ', ' + name.get_first_name(),
# Doe, John
lambda name: \
' '.join(filter(None,
[name.get_surname_prefix(), name.get_surname()])) \
+ ', ' + name.get_first_name().split()[0],
# Doe, J.C.
lambda name: \
' '.join(filter(None,
[name.get_surname_prefix(), name.get_surname()])) \
+ ', ' + \
''.join(map(lambda x:x[:1]+'.',name.get_first_name().split())),
# Doe, J.
lambda name: \
' '.join(filter(None,
[name.get_surname_prefix(), name.get_surname()])) \
+ ', ' + name.get_first_name()[:1],
# John Chris Doe
lambda name: ' '.join(filter(None, [ \
name.get_first_name(), \
name.get_surname_prefix(), name.get_surname()])), \
# John Doe
lambda name: ' '.join(filter(None, [ \
name.get_first_name().split()[0], \
name.get_surname_prefix(), name.get_surname()])), \
# J.C. Doe
lambda name: ' '.join(filter(None, [ \
''.join(map(lambda x:x[:1]+'.',name.get_first_name().split())), \
name.get_surname_prefix(), name.get_surname()])), \
# J. Doe
lambda name: ' '.join(filter(None, [ \
name.get_first_name()[:1] + '.', \
name.get_surname_prefix(), name.get_surname()])), \
]
def _get_report_nameformatters_sample(self):
"""Return array with sample names for nameformatters"""
# person with all features that make distinguishing names
name = RelLib.Name()
name.set_call_name(_('Joe'))
name.set_first_name(_('John Christian'))
name.set_surname(_('Doe'))
formatters = self.get_report_nameformatters()
return [ formatters[i](name) for i in range(len(formatters)) ]
#-------------------------------------------------------------------------
#
# The Dialog class
#
#-------------------------------------------------------------------------
class GeographicDataDialog(ReportDialog):
def __init__(self,dbstate,uistate,person):
self.database = dbstate.db
self.person = person
name = "map_view"
translated_name = _("Map View (Google Earth export)")
self.options_class = GeographicDataOptions(name)
self.category = CATEGORY_CODE
ReportDialog.__init__(self,dbstate,uistate,person,self.options_class,
name,translated_name)
response = self.window.run()
if response == gtk.RESPONSE_OK:
try:
self.make_report()
except (IOError,OSError),msg:
ErrorDialog(str(msg))
elif response == gtk.RESPONSE_DELETE_EVENT:
return
self.close()
def make_doc_menu(self,active=None):
"""Build a one item menu of document types that are
appropriate for this report."""
self.format_menu = FormatComboBox()
self.format_menu.set()
def make_document(self):
"""Do Nothing. This document will be created in the
make_report routine."""
pass
def setup_style_frame(self):
"""The style frame is not used in this dialog."""
pass
def parse_style_frame(self):
"""The style frame is not used in this dialog."""
pass
def doc_type_changed(self, obj):
"""The doc type is of no importance in this dialog."""
pass
def setup_paper_frame(self):
"""No need for Paper Options in this dialog"""
pass
def parse_paper_frame(self):
"""No need for Paper Options in this dialog"""
pass
def make_report(self):
"""Create the object that will produce the GraphViz file."""
GeoGraphicDataExtract(self.database,self.person,self.options_class)
#------------------------------------------------------------------------
#
# Combo Box classes
#
#------------------------------------------------------------------------
class FormatComboBox(gtk.ComboBox):
"""
Format combo box class.
Trivial class supporting only one format.
"""
def set(self,tables=0,callback=None,obj=None,active=None):
self.store = gtk.ListStore(str)
self.set_model(self.store)
cell = gtk.CellRendererText()
self.pack_start(cell,True)
self.add_attribute(cell,'text',0)
self.store.append(row=["Google Earth (kml)"])
self.set_active(0)
def get_label(self):
return "Google Earth (kml)"
def get_reference(self):
return None
def get_paper(self):
return 1
def get_styles(self):
return 0
def get_ext(self):
return '.kml'
def get_printable(self):
_apptype = _options.formats[self.get_active()][3]
print_label = None
try:
mprog = Mime.get_application(_apptype)
if Utils.search_for(mprog[0]):
print_label = _("Open in %(program_name)s") % { 'program_name':
mprog[1]}
else:
print_label = None
except:
print_label = None
return print_label
def get_clname(self):
return 'kml'
class GraphicsFormatComboBox(gtk.ComboBox):
"""
Format combo box class for graphical (not codegen) report.
"""
def set(self,active=None):
self.store = gtk.ListStore(str)
self.set_model(self.store)
cell = gtk.CellRendererText()
self.pack_start(cell,True)
self.add_attribute(cell,'text',0)
active_index = 0
index = 0
for item in _options.formats:
self.store.append(row=[item[2]])
if active == item[0]:
active_index = index
index = index + 1
self.set_active(active_index)
def get_label(self):
return _options.formats[self.get_active()][2]
def get_reference(self):
return EmptyDoc
def get_paper(self):
return 0
def get_styles(self):
return 0
def get_ext(self):
return '.%s' % _options.formats[self.get_active()][0]
def get_format_str(self):
return _options.formats[self.get_active()][0]
def get_printable(self):
_apptype = _options.formats[self.get_active()][3]
print_label = None
try:
mprog = Mime.get_application(_apptype)
if Utils.search_for(mprog[0]):
print_label = _("Open in %(program_name)s") % { 'program_name':
mprog[1] }
else:
print_label = None
except:
print_label = None
return print_label
def get_clname(self):
return 'print'
#------------------------------------------------------------------------
#
# Empty class to keep the BaseDoc-targeted format happy
#
#------------------------------------------------------------------------
class EmptyDoc:
def __init__(self,styles,type,template,orientation,source=None):
self.print_req = 0
def init(self):
pass
def print_requested(self):
self.print_req = 1
#------------------------------------------------------------------------
#
# Class that extracts the geographical data corresponding to the options
# and sends that to the backend
#
#------------------------------------------------------------------------
class GeoGraphicDataExtract:
# EventType -> icon mapping, see _get_style for details.
# This is used in KML style name and icon filename.
_eventstyles_gender = {
RelLib.EventType.UNKNOWN: '',
RelLib.EventType.ADOPT: 'birth',
RelLib.EventType.BIRTH: 'birth',
RelLib.EventType.DEATH: 'death',
RelLib.EventType.ADULT_CHRISTEN: 'bapt',
RelLib.EventType.BAPTISM: 'bapt',
RelLib.EventType.BURIAL: 'burial',
RelLib.EventType.CREMATION: 'burial',
}
_eventstyles_neutral = {
RelLib.EventType.MARRIAGE: 'marriage',
RelLib.EventType.MARR_CONTR: 'marriage',
RelLib.EventType.MARR_LIC: 'marriage',
#RelLib.EventType.DIVORCE: 'divorce',
#RelLib.EventType.DIV_FILING: 'divorce',
RelLib.EventType.MARR_ALT: 'marriage',
}
def __init__(self,database,person,options_class):
"""
Creates GeoGraphicDataExtract object that produces data for the report.
The arguments are:
database - the GRAMPS database instance
person - currently selected person
options_class - instance of the Options class for this report
This report needs the following parameters (class variables)
that come in the options class.
unknowns: if place has unknown location/event has unknown location,
export it nevertheless
"""
self.database = database
self.start_person = person
# get options
options = options_class.handler.options_dict
self.requireDate = not options['unknowndate']
self.requirePlace = not options['unknownplace']
self.nameformatter = options_class.get_report_nameformatters()[options['titleformat']]
# what persons to include
filter_num = options_class.get_filter_number()
filters = options_class.get_report_filters(person)
self.filter = filters[filter_num]
self.filename = options_class.get_output()
print 'filter is',filter_num, \
', requireDate is', self.requireDate, \
', requirePlace is', self.requirePlace, \
', to filename',self.filename
self._obtain_data()
self._writetobackend()
def _obtain_data(self):
"""we perform the needed lookup in the database.
output needed, a dictionary of placemarks, with index the placehandle
and a placemark being
"""
# all places to be written
self.placemarks = []
# styles used, they may need to be written before places
self.usedstyles = {}
# Get list of wanted persons
plist = self.database.get_person_handles(sort_handles=False)
if self.filter:
ind_list = self.filter.apply(self.database,plist)
else:
ind_list = plist
# Gather events of persons, gather families.
# We want one placemark for each related event, that's why events
# and families are gathered first, and then placemark date is
# generated for each unique event.
family_list = {}
eventref_person_list = {}
for person_handle in ind_list:
self.obtain_events_person(person_handle)
# pretty title
self.title = self.filter.get_name()
def obtain_events_person(self, person_handle):
print 'get events for person ' , person_handle
person = self.database.get_person_from_handle(person_handle)
if not person:
return
event_ref_list = person.get_event_ref_list()
for event_ref in event_ref_list:
self.make_event_placemark(event_ref, person)
def make_event_placemark(self, event_ref, person):
#the event_ref holds the role the person plays in an event
event = self.database.get_event_from_handle(event_ref.get_reference_handle())
place = self.database.get_place_from_handle(event.get_place_handle())
date = event.get_date_object()
# data is gathered here, this is passed to the writer class
placemark = {}
placemark['title'] = self._get_display_name(person)
if date:
if date.is_regular():
placemark['date'] = str(date)
elif date.is_compound():
dateparts = str(date).split(' - ')
placemark['date'] = dateparts[0]
placemark['dateend'] = dateparts[1]
# description pop-up
placetitle = ''
if place:
placetitle = place.get_title()
placemark['longdesc_html'] = _('%(name)s
' \
+ 'Event: %(eventtype)s, '\
+ 'Role: %(role)s
' \
+ 'Place: %(place)s
') %{ \
'name': saxutils.escape( \
_nd.display_name(person.get_primary_name())), \
'eventtype': saxutils.escape(str(event.get_type())), \
'role': saxutils.escape(str(event_ref.get_role())), \
'place': saxutils.escape(placetitle) }
if date:
placemark['longdesc_html'] += _('Date: %s') % \
saxutils.escape(str(date))
placemark['longdesc_html'] += '
' + \
saxutils.escape(event.get_description())
# short description with label & place attributes
placemark['shortdesc'] = ''
if event.get_description():
placemark['shortdesc'] += event.get_description()
if place:
if place.get_title():
if placemark['shortdesc']:
placemark['shortdesc'] += '\n'
placemark['shortdesc'] += place.get_title()
# location
placemark['lon'] = place.get_longitude()
placemark['lat'] = place.get_latitude()
placemark['address'] = \
self._location2address(place.get_main_location())
if not placemark['address']:
placemark['address'] = place.get_title()
# style
# TODO primary person should determine style; do we need to
# include events with people who aren't the primary person?
placemark['style'] = self._get_style(person, event)
# we add to dictionary with every place occuring only once
self._add_placemark(placemark)
def _add_placemark(self, placemark) :
"""
Adds a placemark to the list.
TODO: currently it's just added. But when no dates are used,
we may want to have a single place for multiple events.
@param placemark: dictionary with placemark data
"""
# filter out unwanted items
if self.requirePlace:
if not placemark.has_key('address') and \
( not placemark.has_key('lon') or \
not placemark.has_key('lat') ):
return None;
if self.requireDate:
if not placemark.has_key('date'):
return None;
self.usedstyles[placemark['style']] = True
self.placemarks.append(placemark)
return placemark
def _get_style(self, person, event):
type = int(event.get_type())
# try non gender-related first
if self._eventstyles_neutral.has_key(type):
print ' got neutral: ',self._eventstyles_neutral[type]
return self._eventstyles_neutral[type]
# gender-related style, fallback to UNKNOWN
if not self._eventstyles_gender.has_key(type):
print ' falling back to unknown'
type = RelLib.EventType.UNKNOWN
if person.get_gender() is RelLib.Person.MALE:
return 'M'+self._eventstyles_gender[type]
if person.get_gender() is RelLib.Person.FEMALE:
return 'F'+self._eventstyles_gender[type]
print ' got unknown: ',self._eventstyles_gender[type]
return 'U'+self._eventstyles_gender[type]
def _get_display_name(self, person):
"""
Returns the display name for a person, for use as title exported
file.
@param person: person to get name for
@type person: RelLib.Person
@return: name string
"""
return self.nameformatter(person.get_primary_name());
def _location2address(self, location):
"""
Returns an address string for a GRAMPS location. Country is required.
@param location: where
@type location: RelLib.Location
@return: address string, or None
"""
address = ''
if not location:
return None
if location.get_street():
address += location.get_street() + ', '
if location.get_city():
address += location.get_city() + ', '
if location.get_state():
address += location.get_state() + ', '
if location.get_postal_code():
address += location.get_postal_code() + ', '
# country is required
if location.get_country():
address += location.get_country()
else:
return None
return address
def _writetobackend(self):
""" write to the actual backend by calling it's writer correctly
"""
kmlfile = KmlWriter(self.title, self.placemarks, self.usedstyles)
kmlfile.write(self.filename)
#------------------------------------------------------------------------
#
# Report registration
#
#------------------------------------------------------------------------
register_report(
name = 'kmlexport',
category = CATEGORY_CODE,
report_class = GeographicDataDialog,
options_class = GeographicDataOptions,
modes = MODE_GUI ,
translated_name = _("KML Google Earth Export"),
status = _("UnStable"),
description = _("Creates a kml file that can be played/opened in Google Earth."),
author_name = "Benny Malengier, Willem van Engen",
author_email = "bm@cage.UGent.be, dev-gramps@willem.engen.nl"
)
# vim:expandtab:ts=4:sw=4: