# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2001-2005 Donald N. Allingham # # 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$ #------------------------------------------------------------------------- # # Python modules # #------------------------------------------------------------------------- from gettext import gettext as _ import gc import re import logging log = logging.getLogger(".") #------------------------------------------------------------------------- # # GTK/Gnome modules # #------------------------------------------------------------------------- import gobject import gtk import gtk.gdk #------------------------------------------------------------------------- # # Gramps Modules # #------------------------------------------------------------------------- import RelLib import PageView glob_loc_data = [ # (Name, longitude, latitude) ("_Center", 0,0), ("_North",0,90), ("_South",0,-90), ("_West",-180,0), ("_East",180,0), ("Chicago",-87.75,41.83), ("Berlin",13.42,52.53), ("Honolulu",-157.83,21.32), ("Madrid",-3.72,40.42), ("Moscow",37.70,55.75), ("Vienna",16.37,48.22), ("Sidney",151.17,-33.92), ("Rio de Janeiro",-43.28,-22.88), ("Tokyo",139.75,35.67), ("Cape Town",18.47,-33.93), ("Anchorage",-150.00,61.17)] # Draws a map image and tries to allocate space in the correct aspect ratio class GuideMap(gtk.DrawingArea): def __init__(self, map_pixbuf): gtk.DrawingArea.__init__(self) self.map_pixbuf = map_pixbuf self.connect("expose-event", self.expose_cb) self.connect("size-allocate", self.size_allocate_cb) self.gc = None self.current_area = None self.old_size = (-1,-1) # Set hightlight region def hightlight_area( self, area): self.current_area = area self.queue_draw() # Redraw the image def expose_cb(self,widget,event): if not self.gc: self.gc = self.window.new_gc() self.gc.set_foreground( self.get_colormap().alloc_color("red")) self.gc.set_background( self.get_colormap().alloc_color("blue")) if self.backbuf and self.gc: self.window.draw_pixbuf( self.gc, self.backbuf, 0,0, 0,0, -1,-1) if self.current_area: r = self.map_to_screen(self.current_area[0], self.current_area[1], self.current_area[2], self.current_area[3]) self.window.draw_rectangle( self.gc, False, r[0],r[1],r[2],r[3]) # Scale backbuffer def size_allocate_cb(self,widget,allocation): # Always request a height, that is half of the width w = max( 128,allocation.width) self.set_size_request(-1,w/2) # only create new backbuffer if size is different new_size = (allocation.width,allocation.height) if new_size is not self.old_size: self.old_size = new_size self.backbuf = self.map_pixbuf.scale_simple( self.old_size[0], self.old_size[1], gtk.gdk.INTERP_BILINEAR) gc.collect() def map_to_screen( self, x,y,w,h): px = int((float(x) + 180.0) / 360.0 * self.backbuf.get_width()) py = int((90-float(y)) / 180.0 * self.backbuf.get_height()) pw = int(float(w) / 360.0 * self.backbuf.get_width()) ph = int(float(h) / 180.0 * self.backbuf.get_height()) return (px,py,pw,ph) # Zoomable map image class ZoomMap( gtk.DrawingArea): def __init__(self, map_pixbuf, place_marker_pixbuf, hightlight_marker_pixbuf): gtk.DrawingArea.__init__(self) self.map_pixbuf = map_pixbuf self.place_marker_pixbuf = place_marker_pixbuf self.hightlight_marker_pixbuf = hightlight_marker_pixbuf self.connect("expose-event", self.expose_cb) self.connect("size-allocate", self.size_allocate_cb) self.gc = None self.old_size = (-1,-1) self.zoom_pos = (0,0) self.current_area = (0,0,0,0) self.magnifer = 0.5 self.guide = None self.textlayout = self.create_pango_layout("") # Set the guide map that should follow the zoom area def set_guide( self, guide): self.guide = guide def set_location_model( self, model, idx_name, idx_long, idx_lat): self.location_model = model self.idx_name = idx_name self.idx_long = idx_long self.idx_lat = idx_lat # Redraw the image def expose_cb(self,widget,event): if not self.gc: self.gc = self.window.new_gc() self.gc.set_foreground( self.get_colormap().alloc_color("red")) self.gc.set_background( self.get_colormap().alloc_color("blue")) if not self.backbuf: self.size_allocate_cb( self,self.get_allocation()) if self.backbuf and self.gc: self.window.draw_pixbuf( self.gc, self.backbuf, 0,0, 0,0, -1,-1) # draw all available locations if self.location_model: iter = self.location_model.get_iter_first() while iter: (n,x,y) = self.location_model.get( iter, self.idx_name, self.idx_long, self.idx_lat) (px,py) = self.map_to_screen( x, y) if px > 0 and py > 0 and px < self.backbuf.get_width() and py < self.backbuf.get_height(): # draw only visible markers #self.window.draw_pixbuf( # self.gc, # self.place_marker_pixbuf, # 0,0, # px-self.place_marker_pixbuf.get_width()/2, # py-self.place_marker_pixbuf.get_height()/2, # -1,-1) self.textlayout.set_text(n) self.window.draw_layout( self.gc, px,py, self.textlayout) iter = self.location_model.iter_next( iter) # hightlight current location (px,py) = self.map_to_screen( self.zoom_pos[0], self.zoom_pos[1]) self.window.draw_pixbuf( self.gc, self.hightlight_marker_pixbuf, 0,0, px-self.hightlight_marker_pixbuf.get_width()/2, py-self.hightlight_marker_pixbuf.get_height()/2, -1,-1) #self.window.draw_rectangle( self.gc, False, px-3,py-3, 6,6) def map_to_screen( self, long, lat): px = int(self.backbuf.get_width() / self.current_map_area[2] * (float(long) - self.current_map_area[0])) py = int(self.backbuf.get_height() / self.current_map_area[3] * (-float(lat) + self.current_map_area[1])) return( px, py) # Scale backbuffer def size_allocate_cb(self,widget,allocation): # only create new backbuffer if size is different new_size = (allocation.width,allocation.height) if new_size is not self.old_size or not self.backbuf: self.old_size = new_size # Desired midpoint in map pw = int(self.old_size[0]*self.magnifer) ph = int(self.old_size[1]*self.magnifer) px = int((float(self.zoom_pos[0]) + 180.0) / 360.0 * self.map_pixbuf.get_width()) py = int((90-float(self.zoom_pos[1])) / 180.0 * self.map_pixbuf.get_height()) px = max( pw/2, px) py = max( ph/2, py) px = min( px, self.map_pixbuf.get_width()-1-pw/2) py = min( py, self.map_pixbuf.get_height()-1-ph/2) zoomebuf = self.map_pixbuf.subpixbuf( max(0,int(px-pw/2)),max(0,int(py-ph/2)), min(self.map_pixbuf.get_width(),pw), min(self.map_pixbuf.get_height(),ph)) self.backbuf = zoomebuf.scale_simple(self.old_size[0], self.old_size[1], gtk.gdk.INTERP_BILINEAR) gc.collect() mx = 360.0 / self.map_pixbuf.get_width() * (px-pw/2.0) - 180.0 my = 90.0 - 180.0 / self.map_pixbuf.get_height() * (py-ph/2.0) mw = 360.0 / self.map_pixbuf.get_width() * pw mh = 180.0 / self.map_pixbuf.get_height() * ph self.current_area = (px-pw/2, py-ph/2, pw,ph) self.current_map_area = (mx, my, mw, mh) if self.guide: self.guide.hightlight_area( (mx,my,mw,mh)) # Scroll to requested position def scroll_to( self, long, lat): self.zoom_pos = ( min(180,(max(-180,long))), min(90,(max(-90,lat)))) self.backbuf = None self.queue_draw() def zoom_out(self): self.magnifer = min( 10, self.magnifer * 1.5) self.backbuf = None self.queue_draw() def zoom_in(self): self.magnifer = max( 0.1, self.magnifer * 0.75) self.backbuf = None self.queue_draw() def zoom_normal(self): self.magnifer = 1 self.backbuf = None self.queue_draw() def zoom_fit(self): self.magnifer = 2 self.backbuf = None self.queue_draw() # Place list widget class MapPlacesList(gtk.TreeView): def __init__(self, data): self.lstore = gtk.ListStore( gobject.TYPE_STRING, gobject.TYPE_FLOAT, gobject.TYPE_FLOAT) self.change_data( data) gtk.TreeView.__init__(self, self.lstore) self.set_rules_hint(True) self.set_search_column(0) column = gtk.TreeViewColumn('Place', gtk.CellRendererText(), text=0) column.set_sort_column_id(0) self.append_column(column) column = gtk.TreeViewColumn('Lat', gtk.CellRendererText(), text=1) column.set_sort_column_id(1) self.append_column(column) column = gtk.TreeViewColumn('Long', gtk.CellRendererText(),text=2) column.set_sort_column_id(2) self.append_column(column) def change_data( self, data): self.lstore.clear() for item in data: iter = self.lstore.append() self.lstore.set(iter, 0, item[0], 1, item[1], 2, item[2]) # Map View main class class MapView(PageView.PageView): def __init__(self,dbstate,uistate): PageView.PageView.__init__(self,'Map View',dbstate,uistate) dbstate.connect('database-changed',self.change_db) self.current_marker = None def navigation_type(self): return PageView.NAVIGATION_NONE def define_actions(self): self.add_action('ZoomIn',gtk.STOCK_ZOOM_IN, "Zoom _In",callback=self.zoom_in_cb) self.add_action('ZoomOut',gtk.STOCK_ZOOM_OUT, "Zoom _Out",callback=self.zoom_out_cb) self.add_action('ZoomNormal',gtk.STOCK_ZOOM_100, "_Normal Size", callback=self.zoom_100_cb) self.add_action('ZoomFit',gtk.STOCK_ZOOM_FIT, "Best _Fit",callback=self.zoom_fit_cb) def get_stock(self): """ Returns the name of the stock icon to use for the display. This assumes that this icon has already been registered with GNOME as a stock icon. """ return 'gramps-map' # For debugging: Reads in location from xearth def get_xearth_markers(self): data = [] try: f = open("/etc/xearth/xearth.markers") l = f.readline() #linere = re.compile('[^0-9.-]*(-?[0-9]+\.[0-9]+)[^0-9.-]*(-?[0-9]+\.[0-9]+).*"([^"])".*', "I") while l: if not l[0] == "#": l = l.strip().replace('"',"").replace(" "," ").replace(" "," ").replace(" "," ").replace(" # ",", ") m = l.split( None, 2) if len(m) == 3: data.append( (m[2],float(m[1]),float(m[0]))) l = f.readline() except IOError: pass return data # Reads in locations from current GRAMPS database def get_markers_from_database(self, db): data = [] for place_handle in db.get_place_handles(): place = db.get_place_from_handle( place_handle) if place: try: data.append( (place.get_title(),float(place.get_longitude()),float(place.get_latitude()))) except (TypeError, ValueError): # ignore places that dont have usable data pass return data # Reads in textfiles from NIMA: # http://earth-info.nga.mil/gns/html/cntry_files.html def parse_nima_countryfile(self, filename): import csv data = [] try: csvreader = csv.reader(open(filename), "excel-tab") l = csvreader.next() # skip header l = csvreader.next() line = 1 while l: if l[17] == "N" and l[9] == "P": city = l[22] lat = float( l[3]) lon = float( l[4]) if line % 10 == 0: data.append( (city, lon, lat)) l = csvreader.next() line = line + 1 except (IOError,StopIteration): pass return data def build_widget(self): hbox = gtk.HBox( False, 4) hbox.set_border_width( 4) # The large zoomable map self.zoom_map = ZoomMap( gtk.gdk.pixbuf_new_from_file("land_shallow_topo_2048.jpg"), gtk.gdk.pixbuf_new_from_file("bad.png"), gtk.gdk.pixbuf_new_from_file("good.png")) self.zoom_map.set_size_request(300,300) hbox.pack_start( self.zoom_map, True, True, 0) # On the right side vbox = gtk.VBox( False, 4) hbox.pack_start( vbox, False, False, 0) # The small guide map self.guide_map = GuideMap( gtk.gdk.pixbuf_new_from_file("land_shallow_topo_350.jpg")) self.guide_map.set_size_request(128,64) vbox.pack_start( self.guide_map, False, True, 0) self.zoom_map.set_guide(self.guide_map) # and the place list self.place_list_view = MapPlacesList( []) self.zoom_map.set_location_model(self.place_list_view.get_model(), 0,1,2) self.place_list_view.connect("cursor-changed", self.entry_select_cb) self.place_list_view.set_size_request(128,-1) vport = gtk.ScrolledWindow() vbox.pack_start(vport,True,True,0) vport.add( self.place_list_view) self.rebuild_places() return hbox def ui_definition(self): """ Specifies the UIManager XML code that defines the menus and buttons associated with the interface. """ return ''' ''' def change_db(self,db): """ Callback associated with DbState. Whenenver the database changes, this task is called. In this case, we rebuild the columns, and connect signals to the connected database. Tere is no need to store the database, since we will get the value from self.state.db """ db.connect('place-rebuild',self.rebuild_places) db.connect('place-update',self.rebuild_places) def rebuild_places(self,handle_list=None): d = glob_loc_data try: d = d + self.get_xearth_markers() #d = self.parse_nima_countryfile("/tmp/gm.txt") d = d + self.get_markers_from_database( self.dbstate.db) except: log.error("Failed to rebuild places.", exc_info=True) self.place_list_view.change_data( d) def entry_select_cb(self,treeview): s = treeview.get_selection() (model,sel) = s.get_selected_rows() for path in sel: iter = model.get_iter(path) self.zoom_map.scroll_to(model.get_value(iter,1), model.get_value(iter,2)) break def zoom_in_cb(self,obj): self.zoom_map.zoom_in() def zoom_out_cb(self,obj): self.zoom_map.zoom_out() def zoom_100_cb(self,obj): self.zoom_map.zoom_normal() def zoom_fit_cb(self,obj): self.zoom_map.zoom_fit()