diff --git a/configure.in b/configure.in index bc9e74817..5da63348a 100644 --- a/configure.in +++ b/configure.in @@ -151,6 +151,7 @@ src/plugins/gramplet/Makefile src/plugins/graph/Makefile src/plugins/import/Makefile src/plugins/lib/Makefile +src/plugins/mapservices/Makefile src/plugins/quickview/Makefile src/plugins/rel/Makefile src/plugins/textreport/Makefile diff --git a/po/POTFILES.in b/po/POTFILES.in index 57b543816..74d6b0f6d 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -300,6 +300,11 @@ src/plugins/import/ImportVCard.py # plugins/lib directory src/plugins/lib/libholiday.py +src/plugins/lib/libmapservice.py + +# plugins/mapservices directory +src/plugins/mapservices/googlemap.py +src/plugins/mapservices/openstreetmap.py # plugins/quickview directory src/plugins/quickview/AgeOnDate.py @@ -761,6 +766,7 @@ src/FilterEditor/_ShowResults.py src/widgets/buttons.py src/widgets/expandcollapsearrow.py src/widgets/labels.py +src/widgets/menutoolbuttonaction.py src/widgets/styledtexteditor.py src/widgets/validatedmaskedentry.py diff --git a/src/Config/_GrampsConfigKeys.py b/src/Config/_GrampsConfigKeys.py index 6207e3ba4..51a905dd0 100644 --- a/src/Config/_GrampsConfigKeys.py +++ b/src/Config/_GrampsConfigKeys.py @@ -150,6 +150,7 @@ LDS_HEIGHT = ('interface', 'lds-height', 1) LDS_WIDTH = ('interface', 'lds-width', 1) LOCATION_HEIGHT = ('interface', 'location-height', 1) LOCATION_WIDTH = ('interface', 'location-width', 1) +MAPSERVICE = ('interface', 'mapservice', 2) MEDIA_REF_HEIGHT = ('interface', 'media-ref-height', 1) MEDIA_REF_WIDTH = ('interface', 'media-ref-width', 1) URL_HEIGHT = ('interface', 'url-height', 1) @@ -280,7 +281,8 @@ default_value = { LDS_HEIGHT : 450, LDS_WIDTH : 600, LOCATION_HEIGHT : 250, - LOCATION_WIDTH : 600, + LOCATION_WIDTH : 600, + MAPSERVICE : 'OpenStreetMap', MEDIA_REF_HEIGHT : 450, MEDIA_REF_WIDTH : 600, URL_HEIGHT : 150, diff --git a/src/DataViews/PlaceView.py b/src/DataViews/PlaceView.py index 6d1577d91..5de2963aa 100644 --- a/src/DataViews/PlaceView.py +++ b/src/DataViews/PlaceView.py @@ -24,6 +24,13 @@ Place View """ +#------------------------------------------------------------------------- +# +# Global modules +# +#------------------------------------------------------------------------- + + #------------------------------------------------------------------------- # # GTK/Gnome modules @@ -44,6 +51,7 @@ import Errors import Bookmarks import Config from QuestionDialog import ErrorDialog +from gen.plug import PluginManager from DdTargets import DdTargets from Editors import EditPlace, DeletePlaceQuery from Filters.SideBar import PlaceSidebarFilter @@ -97,6 +105,9 @@ class PlaceView(PageView.ListView): 'BackSpace' : self.key_delete, } + self.mapservice = Config.get(Config.MAPSERVICE) + self.mapservicedata = {} + PageView.ListView.__init__( self, _('Places'), dbstate, uistate, PlaceView.COLUMN_NAMES, len(PlaceView.COLUMN_NAMES), @@ -118,39 +129,119 @@ class PlaceView(PageView.ListView): _('_Column Editor'), callback=self._column_editor) self._add_action('FastMerge', None, _('_Merge...'), callback=self.fast_merge) - self._add_action('GoogleMaps', gtk.STOCK_JUMP_TO, _('_Google Maps'), - callback=self.google, - tip=_("Attempt to map location on Google Maps")) + self._add_toolmenu_action('MapsList', 'Loading...', + _("Attempt to see selected locations with a Map " + "Service (OpenstreetMap, Google Maps, ..."), + self.gotomap, + _('Select a Map Service')) + self._add_action('GotoMap', gtk.STOCK_JUMP_TO, + _('_Look up with Map Service'), + callback=self.gotomap, + tip=_("Attempt to see this location with a Map " + "Service (OpenstreetMap, Google Maps, ...")) self._add_action('FilterEdit', None, _('Place Filter Editor'), - callback=self.filter_editor,) + callback=self.filter_editor) + + def change_page(self): + """ + Called by viewmanager at end of realization when arriving on the page + At this point the Toolbar is created. We need to: + 1. get the menutoolbutton + 2. add all possible map services in the drop down menu + 3. add the actions that correspond to clicking in this drop down menu + 4. set icon and label of the menutoolbutton now that it is realized + 5. store label so it can be changed when selection changes + """ + PageView.ListView.change_page(self) + #menutoolbutton actions are stored in PageView class, + # obtain the widgets where we need to add to menu + actionservices = self.action_toolmenu['MapsList'] + widgets = actionservices.get_proxies() + mmenu = self.__create_maps_menu_actions() + + if not self.mapservicedata: + return + if not self.mapservice in self.mapservicedata: + #stored val no longer exists, use the first key instead + self.set_mapservice(self.mapservicedata.keys()[0]) + + #store all gtk labels to be able to update label on selection change + self.mapslistlabel = [] + for widget in widgets : + if isinstance(widget, gtk.MenuToolButton): + widget.set_menu(mmenu) + widget.set_arrow_tooltip_text(actionservices.arrowtooltip) + hbox=gtk.HBox() + img = gtk.Image() + img.set_from_stock(gtk.STOCK_JUMP_TO, + gtk.ICON_SIZE_LARGE_TOOLBAR) + hbox.pack_start(img) + self.mapslistlabel.append(gtk.Label(self.mapservice_label())) + hbox.pack_start(self.mapslistlabel[-1]) + hbox.show_all() + widget.set_icon_widget(hbox) + + def __create_maps_menu_actions(self): + """ + Function creating a menu and actions that are used as dropdown menu + from the menutoolbutton + """ + menu = gtk.Menu() + + #select the map services to show + self.mapservicedata = {} + servlist = PluginManager.get_instance().get_mapservice_list() + for i, service in zip(range(len(servlist)), servlist): + key = service[2].replace(' ', '-') + Utils.add_menuitem(menu, service[1], None, + make_callback(self.set_mapservice, key)) + self.mapservicedata[key] = (service[0], service[2], service[3]) + + return menu + + def set_mapservice(self, mapkey): + """ + change the service that runs on click of the menutoolbutton + used as callback menu on menu clicks + """ + self.mapservice = mapkey + for label in self.mapslistlabel: + label.set_label(self.mapservice_label()) + label.show() + Config.set(Config.MAPSERVICE, mapkey) + Config.sync() + + def mapservice_label(self): + """ + return the current label for the menutoolbutton + """ + return self.mapservicedata[self.mapservice][1] + + def gotomap(self, obj): + """ + Run the map service + """ + place_handles = self.selected_handles() + try: + place_handle = self.selected_handles()[0] + except IndexError: + msg = _("No place selected.") + msg2 = _("You need to select a place to be able to view it" + " on a map. Some Map Services might support multiple" + " selections.") + ErrorDialog(msg, msg2) + return + + #TODO: support for descriptions in some cases. For now, pass None + #TODO: Later this might be 'Birth of William' .... + places = [(x, None) for x in place_handles] + + #run the mapservice: + self.mapservicedata[self.mapservice][0](self.dbstate.db, places) def drag_info(self): return DdTargets.PLACE_LINK - def google(self, obj): - import GrampsDisplay - from PlaceUtils import conv_lat_lon - - try: - place_handle = self.selected_handles()[0] - except IndexError: - return - place = self.dbstate.db.get_place_from_handle(place_handle) - descr = place.get_title() - longitude = place.get_longitude() - latitude = place.get_latitude() - latitude, longitude = conv_lat_lon(latitude, longitude, "D.D8") - city = place.get_main_location().get_city() - country = place.get_main_location().get_country() - - if longitude and latitude: - path = "http://maps.google.com/?sll=%s,%s&z=15" % (latitude, longitude) - elif city and country: - path = "http://maps.google.com/maps?q=%s,%s" % (city, country) - else: - path = "http://maps.google.com/maps?q=%s" % '+'.join(descr.split()) - GrampsDisplay.url(path) - def _column_editor(self, obj): import ColumnOrder @@ -204,14 +295,14 @@ class PlaceView(PageView.ListView): - + - + ''' @@ -271,3 +362,6 @@ class PlaceView(PageView.ListView): return obj.get_handle() else: return None + +def make_callback(func, val): + return lambda x: func(val) diff --git a/src/PageView.py b/src/PageView.py index 4ec04111f..e6c8ef67b 100644 --- a/src/PageView.py +++ b/src/PageView.py @@ -51,6 +51,8 @@ import Errors from Filters import SearchBar import Utils import const +from widgets.menutoolbuttonaction import MenuToolButtonAction + from TransUtils import sgettext as _ from QuestionDialog import QuestionDialog, QuestionDialog2 @@ -75,6 +77,8 @@ class PageView: self.uistate = uistate self.action_list = [] self.action_toggle_list = [] + self.action_toolmenu_list = [] + self.action_toolmenu = {} #easy access to toolmenuaction and proxies self.action_group = None self.additional_action_groups = [] self.additional_uis = [] @@ -206,13 +210,24 @@ class PageView: self.action_group.add_actions(self.action_list) if len(self.action_toggle_list) > 0: self.action_group.add_toggle_actions(self.action_toggle_list) + for action_toolmenu in self.action_toolmenu_list: + self.action_toolmenu[action_toolmenu[0]] = \ + MenuToolButtonAction(action_toolmenu[0], #unique name + action_toolmenu[1], #label + action_toolmenu[2], #tooltip + action_toolmenu[3], #callback + action_toolmenu[4] #arrow tooltip + ) + self.action_group.add_action( + self.action_toolmenu[action_toolmenu[0]]) def _add_action(self, name, stock_icon, label, accel=None, tip=None, callback=None): """ Add an action to the action list for the current view. """ - self.action_list.append((name, stock_icon, label, accel, tip, callback)) + self.action_list.append((name, stock_icon, label, accel, tip, + callback)) def _add_toggle_action(self, name, stock_icon, label, accel=None, tip=None, callback=None, value=False): @@ -221,6 +236,11 @@ class PageView: """ self.action_toggle_list.append((name, stock_icon, label, accel, tip, callback, value)) + + def _add_toolmenu_action(self, name, label, tooltip, callback, + arrowtooltip): + self.action_toolmenu_list.append((name, label, tooltip, callback, + arrowtooltip)) def get_actions(self): """ diff --git a/src/gen/plug/_manager.py b/src/gen/plug/_manager.py index ad2c41314..c32befb8c 100644 --- a/src/gen/plug/_manager.py +++ b/src/gen/plug/_manager.py @@ -96,6 +96,7 @@ class PluginManager(gen.utils.Callback): self.__import_plugins = [] self.__export_plugins = [] self.__general_plugins = [] + self.__mapservice_list = [] self.__attempt_list = [] self.__loaddir_list = [] self.__textdoc_list = [] @@ -240,6 +241,10 @@ class PluginManager(gen.utils.Callback): """ Return the list of quick report plugins. """ return self.__quick_report_list + def get_mapservice_list(self): + """ Return the list of map services""" + return self.__mapservice_list + def get_book_item_list(self): """ Return the list of book plugins. """ return self.__bkitems_list @@ -534,6 +539,30 @@ class PluginManager(gen.utils.Callback): self.__mod2text[run_func.__module__] = description + def register_mapservice(self, name, mapservice, translated_name, + status=_("Unknown"), tooltip=_UNAVAILABLE, + author_name=_("Unknown"), + author_email=_("Unknown"), unsupported=False ): + """ + Register map services for the place view. + A map service is a MapService class. The class can be called with a + list of (place handle, description) values, and manipulate this data + to show on a map. + """ + del_index = -1 + for i in range(0, len(self.__mapservice_list)): + val = self.__mapservice_list[i] + if val[2] == name: + del_index = i + if del_index != -1: + del self.__mapservice_list[del_index] + + self.__mapservice_list.append( (mapservice, translated_name, + name, tooltip, status, + author_name, author_email, unsupported)) + + self.__mod2text[mapservice.__module__] = tooltip + def __purge_failed(self): """ Purge the failed plugins from the corresponding lists. diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am index 47a5b364b..faa3d129a 100644 --- a/src/plugins/Makefile.am +++ b/src/plugins/Makefile.am @@ -11,6 +11,7 @@ SUBDIRS = \ graph \ import \ lib \ + mapservices \ quickview \ rel \ textreport \ diff --git a/src/plugins/lib/Makefile.am b/src/plugins/lib/Makefile.am index ed3970b9c..5ef3c56fd 100644 --- a/src/plugins/lib/Makefile.am +++ b/src/plugins/lib/Makefile.am @@ -6,7 +6,8 @@ pkgdatadir = $(datadir)/@PACKAGE@/plugins/lib pkgdata_PYTHON = \ - libholiday.py + libholiday.py\ + libmapservices.py pkgpyexecdir = @pkgpyexecdir@/plugins/lib pkgpythondir = @pkgpythondir@/plugins/lib diff --git a/src/plugins/lib/libmapservice.py b/src/plugins/lib/libmapservice.py index 80b9eb9f7..db46b3d2d 100644 --- a/src/plugins/lib/libmapservice.py +++ b/src/plugins/lib/libmapservice.py @@ -22,6 +22,13 @@ Google Maps map service. Open place in maps.google.com """ +#------------------------------------------------------------------------ +# +# python modules +# +#------------------------------------------------------------------------ +from gettext import gettext as _ + #------------------------------------------------------------------------- # # GRAMPS modules @@ -36,7 +43,11 @@ class MapService(): A service is a singleton, we only need one to exist Usage is as a callable when used """ - + def __init__(self): + self.database = None + self.items = None + self.url = '' + def __call__(self, database, items): """Callable class, usable as a function. This guarantees the class is instantiated once when a service is registered. Afterward only calls @@ -45,11 +56,11 @@ class MapService(): items: list of tuples (place_handle, description), where description is None or a string to use for marker (eg 'birth John Doe') """ - self.db = database + self.database = database self.items = items self.url = '' #An instance is called, we display the result - self._calc_url() + self.calc_url() self.__display() self._free() @@ -57,23 +68,24 @@ class MapService(): """Obtain the first place object""" place_handle = self.items[0][0] - return self.db.get_place_from_handle(place_handle), self.items[0][1] + return self.database.get_place_from_handle(place_handle), \ + self.items[0][1] def _all_places(self): """Obtain a list generator of all place objects Usage: for place, descr in mapservice.all_places() """ - for handle, descr in db.get_handles(): - yield self.db.get_place_from_handle(handle), descr + for handle, descr in self.database.get_handles(): + yield self.database.get_place_from_handle(handle), descr def _lat_lon(self, place, format="D.D8"): """return the lat, lon value of place in the requested format None, None if invalid """ - return PlaceUtils.conv_lat_lon(place.get_latitude(), + return conv_lat_lon(place.get_latitude(), place.get_longitude(), format) - def _calc_url(self): + def calc_url(self): """Base class needs to overwrite this, calculation of the self.path""" raise NotImplementedError diff --git a/src/plugins/mapservices/googlemap.py b/src/plugins/mapservices/googlemap.py new file mode 100644 index 000000000..3b77dbe09 --- /dev/null +++ b/src/plugins/mapservices/googlemap.py @@ -0,0 +1,84 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2009 Benny Malengier +# +# 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 +# + +""" +Google Maps map service plugin. Open place in maps.google.com +""" + +#------------------------------------------------------------------------ +# +# python modules +# +#------------------------------------------------------------------------ +from gettext import gettext as _ + +#------------------------------------------------------------------------ +# +# GRAMPS modules +# +#------------------------------------------------------------------------ +from gen.plug import PluginManager +from libmapservice import MapService + + +class GoogleMapService(MapService): + """Map service using http://maps.google.com""" + def __init__(self): + MapService.__init__(self) + + def calc_url(self): + """ Determine the url to use on maps.google.com + Logic: use lat lon if present + otherwise use city and country if present + otherwise use description of the place + """ + place = self._get_first_place()[0] + latitude, longitude = self._lat_lon(place) + if longitude and latitude: + self.url = "http://maps.google.com/?sll=%s,%s&z=15" % (latitude, + longitude) + return + + city = place.get_main_location().get_city() + country = place.get_main_location().get_country() + if city and country: + self.url = "http://maps.google.com/maps?q=%s,%s" % (city, country) + return + + titledescr = place.get_title() + self.url = "http://maps.google.com/maps?q=%s" % \ + '+'.join(titledescr.split()) + + +#------------------------------------------------------------------------ +# +# Register map service +# +#------------------------------------------------------------------------ + +PluginManager.get_instance().register_mapservice( + name = 'GoogleMaps', + mapservice = GoogleMapService(), + translated_name = _("GoogleMaps"), + status = _("Stable"), + tooltip= _("Open on maps.google.com"), + author_name="Benny Malengier", + author_email="benny.malengier@gramps-project.org" + ) diff --git a/src/plugins/mapservices/openstreetmap.py b/src/plugins/mapservices/openstreetmap.py new file mode 100644 index 000000000..da19891b5 --- /dev/null +++ b/src/plugins/mapservices/openstreetmap.py @@ -0,0 +1,88 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2009 Benny Malengier +# +# 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 +# + +""" +OpenStreetMap map service plugin. Open place in openstreetmap.org +""" + +#------------------------------------------------------------------------ +# +# python modules +# +#------------------------------------------------------------------------ +from gettext import gettext as _ + +#------------------------------------------------------------------------ +# +# GRAMPS modules +# +#------------------------------------------------------------------------ +from gen.plug import PluginManager +from libmapservice import MapService + + +class OpensStreetMapService(MapService): + """Map service using http://openstreetmap.org + Resource: http://wiki.openstreetmap.org/index.php/Name_finder + """ + def __init__(self): + MapService.__init__(self) + + def calc_url(self): + """ Determine the url to use + Logic: use lat lon if present + otherwise use city and country if present + otherwise use description of the place + """ + place = self._get_first_place()[0] + latitude, longitude = self._lat_lon(place) + if longitude and latitude: + self.url = "http://www.openstreetmap.org/" \ + "?lat=%s&lon=%s&zoom=15" % (latitude, longitude) + + return + + city = place.get_main_location().get_city() + country = place.get_main_location().get_country() + if city and country: + self.url = "http://gazetteer.openstreetmap.org/namefinder/"\ + "?find=%s%%2C%s" % (city, country) + return + + titledescr = place.get_title() + self.url = "http://gazetteer.openstreetmap.org/namefinder/"\ + "?find=%s" % '+'.join(titledescr.split()) + + +#------------------------------------------------------------------------ +# +# Register map service +# +#------------------------------------------------------------------------ + +PluginManager.get_instance().register_mapservice( + name = 'OpenStreetMap', + mapservice = OpensStreetMapService(), + translated_name = _("OpenStreetMap"), + status = _("Stable"), + tooltip= _("Open on openstreetmap.org"), + author_name="Benny Malengier", + author_email="benny.malengier@gramps-project.org" + ) diff --git a/src/widgets/Makefile.am b/src/widgets/Makefile.am index 0e96b4bab..5d8ff468f 100644 --- a/src/widgets/Makefile.am +++ b/src/widgets/Makefile.am @@ -11,6 +11,7 @@ pkgdata_PYTHON = \ expandcollapsearrow.py \ labels.py \ linkbox.py \ + menutoolbuttonaction.py \ monitoredwidgets.py \ shortlistcomboentry.py \ springseparator.py \ diff --git a/src/widgets/menutoolbuttonaction.py b/src/widgets/menutoolbuttonaction.py new file mode 100644 index 000000000..69542aefb --- /dev/null +++ b/src/widgets/menutoolbuttonaction.py @@ -0,0 +1,85 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2009 Benny Malengier +# +# 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: menutoolbuttonaction 10763 2008-05-27 19:53:25Z zfoldvar $ + +"MenuToolButtonAction class." + +__all__ = ["MenuToolButtonAction"] + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".widgets.menutoolbuttonaction") + +#------------------------------------------------------------------------- +# +# GTK modules +# +#------------------------------------------------------------------------- +import gobject +import gtk + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- + + +#------------------------------------------------------------------------- +# +# ValueAction class +# +#------------------------------------------------------------------------- +class MenuToolButtonAction(gtk.Action): + """MenuToolButton action class. + + (A MenuToolButtonAction with menu item doesn't make any sense, + use for toolbar.) + + """ + __gtype_name__ = "MenuToolButtonAction" + + __gsignals__ = { + 'changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, #return value + ()), # arguments + } + + def __init__(self, name, label, tooltip, callback, arrowtooltip): + """Create a new MenuToolButtonAction instance. + + @param name: the name of the action + @type name: str + @param tooltip: tooltip string + @type tooltip: str + + """ + gtk.Action.__init__(self, name, label, tooltip, None) + + self.set_tool_item_type(gtk.MenuToolButton) + if callback: + self.connect('activate', callback) + self.arrowtooltip = arrowtooltip +