Logo Search packages:      
Sourcecode: d-rats version File versions  Download package

mapdisplay.py

#!/usr/bin/python

import os
import math
import urllib
import time
import random
import shutil
import tempfile
import threading
import copy

import gtk
import gobject

import mainapp
import platform
import miscwidgets
import inputdialog
import utils
import geocode_ui
import map_sources
import map_source_editor
import signals

from ui.main_common import ask_for_confirmation

from gps import GPSPosition, distance, value_with_units, DPRS_TO_APRS

CROSSHAIR = "+"

COLORS = ["red", "green", "cornflower blue", "pink", "orange", "grey"]

BASE_DIR = None

def set_base_dir(basedir):
    global BASE_DIR

    BASE_DIR = basedir

CONFIG = None

CONNECTED = True
MAX_TILE_LIFE = 0
PROXY = None

def set_connected(connected):
    global CONNECTED

    CONNECTED = connected

def set_tile_lifetime(lifetime):
    global MAX_TILE_LIFE

    MAX_TILE_LIFE = lifetime

def set_proxy(proxy):
    global PROXY

    PROXY = proxy

def fetch_url(url, local):
    global CONNECTED
    global PROXY

    if not CONNECTED:
        raise Exception("Not connected")

    if PROXY:
        proxies = {"http" : PROXY}
    else:
        proxies = None

    data = urllib.urlopen(url, proxies=proxies)
    local_file = file(local, "wb")
    d = data.read()
    local_file.write(d)
    data.close()
    local_file.close()

00081 class MarkerEditDialog(inputdialog.FieldDialog):
    def __init__(self):
        inputdialog.FieldDialog.__init__(self, title=_("Add Marker"))

        self.icons = []
        for sym in sorted(DPRS_TO_APRS.values()):
            icon = utils.get_icon(sym)
            if icon:
                self.icons.append((icon, sym))

        self.add_field(_("Group"), miscwidgets.make_choice([], True))
        self.add_field(_("Name"), gtk.Entry())
        self.add_field(_("Latitude"), miscwidgets.LatLonEntry())
        self.add_field(_("Longitude"), miscwidgets.LatLonEntry())
        self.add_field(_("Lookup"), gtk.Button("By Address"))
        self.add_field(_("Comment"), gtk.Entry())
        self.add_field(_("Icon"), miscwidgets.make_pixbuf_choice(self.icons))

        self._point = None

    def set_groups(self, groups, group=None):
        grpsel = self.get_field(_("Group"))
        for grp in groups:
            grpsel.append_text(grp)

        if group is not None:
            grpsel.child.set_text(group)
            grpsel.set_sensitive(False)
        else:
            grpsel.child.set_text(_("Misc"))

    def get_group(self):
        return self.get_field(_("Group")).child.get_text()

    def set_point(self, point):
        self.get_field(_("Name")).set_text(point.get_name())
        self.get_field(_("Latitude")).set_text("%.4f" % point.get_latitude())
        self.get_field(_("Longitude")).set_text("%.4f" % point.get_longitude())
        self.get_field(_("Comment")).set_text(point.get_comment())

        iconsel = self.get_field(_("Icon"))
        if isinstance(point, map_sources.MapStation):
            symlist = [y for x,y in self.icons]
            try:
                iidx = symlist.index(point.get_aprs_symbol())
                iconsel.set_active(iidx)
            except ValueError:
                print "No such symbol `%s'" % point.get_aprs_symbol()

        else:
            iconsel.set_sensitive(False)

        self._point = point
                       
    def get_point(self):
        name = self.get_field(_("Name")).get_text()
        lat = self.get_field(_("Latitude")).value()
        lon = self.get_field(_("Longitude")).value()
        comment = self.get_field(_("Comment")).get_text()
        idx = self.get_field(_("Icon")).get_active()
        
        self._point.set_name(name)
        self._point.set_latitude(lat)
        self._point.set_longitude(lon)
        self._point.set_comment(comment)

        if isinstance(self._point, map_sources.MapStation):
            self._point.set_icon_from_aprs_sym(self.icons[idx][1])

        return self._point

# These functions taken from:
#   http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
def deg2num(lat_deg, lon_deg, zoom):
  lat_rad = lat_deg * math.pi / 180.0
  n = 2.0 ** zoom
  xtile = int((lon_deg + 180.0) / 360.0 * n)
  ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n)
  return(xtile, ytile)

def num2deg(xtile, ytile, zoom):
  n = 2.0 ** zoom
  lon_deg = xtile / n * 360.0 - 180.0
  lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
  lat_deg = lat_rad * 180.0 / math.pi
  return(lat_deg, lon_deg)

00168 class MapTile(object):
    def path_els(self):
        return deg2num(self.lat, self.lon, self.zoom)

    def tile_edges(self):
        n, w = num2deg(self.x, self.y, self.zoom)
        s, e = num2deg(self.x+1, self.y+1, self.zoom)

        return (s, w, n, e)

    def lat_range(self):
        s, w, n, e = self.tile_edges()
        return (n, s)

    def lon_range(self):
        s, w, n, e = self.tile_edges()
        return (w, e)

    def path(self):
        return "%d/%d/%d.png" % (self.zoom, self.x, self.y)

    def _local_path(self):
        path = os.path.join(self.dir, self.path())
        if not os.path.isdir(os.path.dirname(path)):
            os.makedirs(os.path.dirname(path))

        return path

    def is_local(self):
        if MAX_TILE_LIFE == 0 or not CONNECTED:
            return os.path.exists(self._local_path())
        else:
            try:
                ts = os.stat(self._local_path()).st_mtime
                return (time.time() - ts) < MAX_TILE_LIFE
            except OSError:
                return False

    def fetch(self):
        if not self.is_local():
            for i in range(10):
                url = self.remote_path()
                try:
                    fetch_url(url, self._local_path())
                    return True
                except Exception, e:
                    print "[%i] Failed to fetch `%s': %s" % (i, url, e)

            return False
        else:
            return True

    def _thread(self, cb, *args):
        if self.fetch():
            fname = self._local_path()
        else:
            fname = None

        gobject.idle_add(cb, fname, *args)
        
    def threaded_fetch(self, cb, *args):
        _args = (cb,) + args
        t = threading.Thread(target=self._thread, args=_args)
        t.setDaemon(True)
        t.start()

    def local_path(self):
        path = self._local_path()
        self.fetch()

        return path

    def remote_path(self):
        return "http://tile.openstreetmap.org/%s" % (self.path())

    def __add__(self, count):
        (x, y) = count

        return MapTile(self.x+x, self.y+y, self.zoom)

    def __sub__(self, tile):
        return (self.x - tile.x, self.y - tile.y)

    def __contains__(self, point):
        (lat, lon) = point

        # FIXME for non-western!
        (lat_max, lat_min) = self.lat_range()
        (lon_min, lon_max) = self.lon_range()

        lat_match = (lat < lat_max and lat > lat_min)
        lon_match = (lon < lon_max and lon > lon_min)

        return lat_match and lon_match

    def __init__(self, lat, lon, zoom):
        self.zoom = zoom
        if isinstance(lat, int) and isinstance(lon, int):
            self.x = lat
            self.y = lon
            self.lat, self.lon = num2deg(self.x, self.y, self.zoom)
        else:
            self.lat = lat
            self.lon = lon
            self.x, self.y = deg2num(self.lat, self.lon, self.zoom)

        if BASE_DIR:
            self.dir = BASE_DIR
        else:
            p = platform.get_platform()
            self.dir = os.path.join(p.config_dir(), "maps")

        if not os.path.isdir(self.dir):
            os.mkdir(self.dir)

    def __str__(self):
        return "%.4f,%.4f (%i,%i)" % (self.lat, self.lon, self.x, self.y)

00286 class LoadContext(object):
    pass

00289 class MapWidget(gtk.DrawingArea):
    __gsignals__ = {
        "redraw-markers" : (gobject.SIGNAL_RUN_LAST,
                            gobject.TYPE_NONE,
                            ()),
        "new-tiles-loaded" : (gobject.SIGNAL_ACTION,
                              gobject.TYPE_NONE,
                              ()),
        }

    def draw_text_marker_at(self, x, y, text, color="yellow"):
        gc = self.get_style().black_gc

        if self.zoom < 12:
            size = 'size="x-small"'
        elif self.zoom < 14:
            size = 'size="small"'
        else:
            size = ''

        text = utils.filter_to_ascii(text)

        pl = self.create_pango_layout("")
        markup = '<span %s background="%s">%s</span>' % (size, color, text)
        pl.set_markup(markup)
        self.window.draw_layout(gc, int(x), int(y), pl)

    def draw_image_at(self, x, y, pb):
        gc = self.get_style().black_gc

        self.window.draw_pixbuf(gc,
                                pb,
                                0, 0,
                                int(x), int(y))

        return pb.get_height()

    def draw_cross_marker_at(self, x, y):
        width = 2
        cm = self.window.get_colormap()
        color = cm.alloc_color("red")
        gc = self.window.new_gc(foreground=color,
                                line_width=width)

        x = int(x)
        y = int(y)

        self.window.draw_lines(gc, [(x, y-5), (x, y+5)])
        self.window.draw_lines(gc, [(x-5, y), (x+5, y)])

    def latlon2xy(self, lat, lon):
        y = 1- ((lat - self.lat_min) / (self.lat_max - self.lat_min))
        x = 1- ((lon - self.lon_min) / (self.lon_max - self.lon_min))
        
        x *= (self.tilesize * self.width)
        y *= (self.tilesize * self.height)

        y += self.lat_fudge
        return (x, y)

    def xy2latlon(self, x, y):
        y -= self.lat_fudge

        lon = 1 - (float(x) / (self.tilesize * self.width))
        lat = 1 - (float(y) / (self.tilesize * self.height))
        
        lat = (lat * (self.lat_max - self.lat_min)) + self.lat_min
        lon = (lon * (self.lon_max - self.lon_min)) + self.lon_min

        return lat, lon

    def draw_marker(self, label, lat, lon, img=None):
        color = "red"

        try:
            x, y = self.latlon2xy(lat, lon)
        except ZeroDivisionError:
            return

        if label == CROSSHAIR:
            self.draw_cross_marker_at(x, y)
        else:
            if img:
                y += (4 + self.draw_image_at(x, y, img))
            self.draw_text_marker_at(x, y, label, color)

    def expose(self, area, event):
        if len(self.map_tiles) == 0:
            self.load_tiles()

        gc = self.get_style().black_gc
        self.window.draw_drawable(gc,
                                  self.pixmap,
                                  0, 0,
                                  0, 0,
                                  -1, -1)
        self.emit("redraw-markers")

    def calculate_bounds(self):
        center = MapTile(self.lat, self.lon, self.zoom)
        topleft = center + (-2, -2)
        botright = center + (2, 2)
        (self.lat_min, _, _, self.lon_min) = botright.tile_edges()
        (_, self.lon_max, self.lat_max, _) = topleft.tile_edges()        

        # I have no idea why, but for some reason we can calculate the
        # longitude (x) just fine, but not the latitude (y).  The result
        # of either latlon2xy() or tile_edges() is bad, which causes the
        # y calculation of a given latitude to be off by some amount.
        # The amount grows large at small values of zoom (zoomed out) and
        # starts to seriously affect the correctness of marker placement.
        # Until I figure out why that is, we calculate a fudge factor below.
        #
        # To do this, we ask the center tile for its NW corner's
        # coordinates.  We then ask latlon2xy() (with fudge of zero) what
        # the corresponding x,y is.  Since we know what the correct value
        # should be, we record the offset and use that to shift the y in
        # further calculations for this zoom level.

        self.lat_fudge = 0
        s, w, n, e = center.tile_edges()
        x, y = self.latlon2xy(n, w)
        self.lat_fudge = ((self.height / 2) * self.tilesize) - y
        if False:
            print "------ Bounds Calculation ------"
            print "Center tile should be at %i,%i" % (\
                (self.height/2) * self.tilesize,
                (self.width/2) * self.tilesize)
            print "We calculate it based on Lat,Lon to be %i, %i" % (x, y)
            print "--------------------------------"
        print "Latitude Fudge Factor: %i (zoom %i)" % (self.lat_fudge,
                                                       self.zoom)

    def broken_tile(self):
        if self.__broken_tile:
            return self.__broken_tile

        broken = [
            "48 16 3 1",
            "       c #FFFFFFFFFFFF",
            "x      c #FFFF00000000",
            "X      c #000000000000",
            "xx             xx   XX   X   XXX                ",
            " xx           xx    X X  X  X   X               ",
            "  xx         xx     X X  X X     X              ",
            "   xx       xx      X  X X X     X              ",
            "    xx     xx       X  X X X     X              ",
            "     xx   xx        X  X X  X   X               ",
            "      xx xx         X   XX   XXX                ",
            "       xxx                                      ",
            "       xxx                                      ",
            "      xx xx         XXXX     XX   XXXXX   XX    ",
            "     xx   xx        X   X   X  X    X    X  X   ",
            "    xx     xx       X    X X    X   X   X    X  ",
            "   xx       xx      X    X X    X   X   X    X  ",
            "  xx         xx     X    X XXXXXX   X   XXXXXX  ",
            " xx           xx    X   X  X    X   X   X    X  ",
            "xx             xx   XXXX   X    X   X   X    X  "
            ]

        # return gtk.gdk.pixbuf_new_from_xpm_data(broken)
        pm = gtk.gdk.pixmap_create_from_xpm_d(self.window, None, broken)[0]
        pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
                            False,
                            8,
                            self.tilesize, self.tilesize)
        pb.fill(0xffffffff)

        x = y = (self.tilesize / 2)

        pb.get_from_drawable(pm, pm.get_colormap(), 0, 0, x, y, -1, -1)

        self.__broken_tile = pb
        
        return pb

    def draw_tile(self, path, x, y, ctx=None):
        if ctx and ctx.zoom != self.zoom:
            # Zoom level has chnaged, so don't do anything
            return

        gc = self.pixmap.new_gc()
        if path:
            try:
                pb = gtk.gdk.pixbuf_new_from_file(path)
            except Exception, e:
                utils.log_exception()
                pb = self.broken_tile()
        else:
            pb = self.broken_tile()

        if ctx:
            ctx.loaded_tiles += 1
            frac = float(ctx.loaded_tiles) / float(ctx.total_tiles)
            if ctx.loaded_tiles == ctx.total_tiles:
                self.status(0.0, "")
            else:
                self.status(frac, _("Loaded") + " %.0f%%" % (frac * 100.0))

        self.pixmap.draw_pixbuf(gc, pb, 0, 0, x, y, -1, -1)
        self.queue_draw()

    @utils.run_gtk_locked
    def draw_tile_locked(self, *args):
        self.draw_tile(*args)

    def load_tiles(self):
        self.map_tiles = []
        ctx = LoadContext()
        ctx.loaded_tiles = 0
        ctx.total_tiles = self.width * self.height
        ctx.zoom = self.zoom

        center = MapTile(self.lat, self.lon, self.zoom)

        delta_h = self.height / 2
        delta_w = self.width  / 2

        count = 0
        total = self.width * self.height

        if not self.window:
            # Window is not loaded, thus can't load tiles
            return

        try:
            self.pixmap = gtk.gdk.Pixmap(self.window,
                                         self.width * self.tilesize,
                                         self.height * self.tilesize)
        except Exception, e:
            # Window is not loaded, thus can't load tiles
            return

        gc = self.pixmap.new_gc()

        for i in range(0, self.width):
            for j in range(0, self.height):
                tile = center + (i - delta_w, j - delta_h)
                if not tile.is_local():
                    message = _("Retrieving")
                else:
                    message = _("Loading")
               
                if tile.is_local():
                    path = tile._local_path()
                    self.draw_tile(tile._local_path(),
                                   self.tilesize * i, self.tilesize * j,
                                   ctx)
                else:
                    self.draw_tile(None, self.tilesize * i, self.tilesize * j)
                    tile.threaded_fetch(self.draw_tile_locked,
                                        self.tilesize * i,
                                        self.tilesize * j,
                                        ctx)

                self.map_tiles.append(tile)

                count += 1

        self.calculate_bounds()

        self.emit("new-tiles-loaded")

    def export_to(self, filename, bounds=None):
        if not bounds:
            x = 0
            y = 0
            bounds = (0,0,-1,-1)
            width = self.tilesize * self.width
            height = self.tilesize * self.height
        else:
            x = bounds[0]
            y = bounds[1]
            width = bounds[2] - bounds[0]
            height = bounds[3] - bounds[1]

        pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width, height)
        pb.get_from_drawable(self.pixmap, self.pixmap.get_colormap(),
                             x, y, 0, 0, width, height)
        pb.save(filename, "png")

    def __init__(self, width, height, tilesize=256, status=None):
        gtk.DrawingArea.__init__(self)

        self.__broken_tile = None

        self.height = height
        self.width = width
        self.tilesize = tilesize
        self.status = status

        self.lat = 0
        self.lon = 0
        self.zoom = 1

        self.lat_max = self.lat_min = 0
        self.lon_max = self.lon_min = 0

        self.map_tiles = []

        self.set_size_request(self.tilesize * self.width,
                              self.tilesize * self.height)
        self.connect("expose-event", self.expose)

    def set_center(self, lat, lon):
        self.lat = lat
        self.lon = lon
        self.map_tiles = []
        self.queue_draw()

    def get_center(self):
        return (self.lat, self.lon)

    def set_zoom(self, zoom):
        if zoom > 17 or zoom == 1:
            return

        self.zoom = zoom
        self.map_tiles = []
        self.queue_draw()

    def get_zoom(self):
        return self.zoom

    def scale(self, x, y, pixels=128):
        shift = 15
        tick = 5

        #rect = gtk.gdk.Rectangle(x-pixels,y-shift-tick,x,y)
        #self.window.invalidate_rect(rect, True)

        (lat_a, lon_a) = self.xy2latlon(self.tilesize, self.tilesize)
        (lat_b, lon_b) = self.xy2latlon(self.tilesize * 2, self.tilesize)

        # width of one tile
        d = distance(lat_a, lon_a, lat_b, lon_b) * (float(pixels) / self.tilesize)

        dist = value_with_units(d)

        color = self.window.get_colormap().alloc_color("black")
        gc = self.window.new_gc(line_width=1, foreground=color)

        self.window.draw_line(gc, x-pixels, y-shift, x, y-shift)
        self.window.draw_line(gc, x-pixels, y-shift, x-pixels, y-shift-tick)
        self.window.draw_line(gc, x, y-shift, x, y-shift-tick)
        self.window.draw_line(gc, x-(pixels/2), y-shift, x-(pixels/2), y-shift-tick)

        pl = self.create_pango_layout("")
        pl.set_markup("%s" % dist)
        self.window.draw_layout(gc, x-pixels, y-shift, pl)        

    def point_is_visible(self, lat, lon):
        for i in self.map_tiles:
            if (lat, lon) in i:
                return True

        return False

00647 class MapWindow(gtk.Window):
    __gsignals__ = {
        "reload-sources" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "user-send-chat" : signals.USER_SEND_CHAT,
        "get-station-list" : signals.GET_STATION_LIST,
        }

    _signals = {"user-send-chat" : None,
                "get-station-list" : None,
                }

    def zoom(self, widget, frame):
        adj = widget.get_adjustment()

        self.map.set_zoom(int(adj.value))
        frame.set_label(_("Zoom") + " (%i)" % int(adj.value))

    def make_zoom_controls(self):
        box = gtk.HBox(False, 3)
        box.set_border_width(3)
        box.show()

        l = gtk.Label(_("Min"))
        l.show()
        box.pack_start(l, 0,0,0)

        adj = gtk.Adjustment(value=14,
                             lower=2,
                             upper=17,
                             step_incr=1,
                             page_incr=3)
        sb = gtk.HScrollbar(adj)
        sb.show()
        box.pack_start(sb, 1,1,1)

        l = gtk.Label(_("Max"))
        l.show()
        box.pack_start(l, 0,0,0)

        frame = gtk.Frame(_("Zoom"))
        frame.set_label_align(0.5, 0.5)
        frame.set_size_request(150, 50)
        frame.show()
        frame.add(box)

        sb.connect("value-changed", self.zoom, frame)

        return frame

    def toggle_show(self, group, *vals):
        if group:
            station = vals[1]
        else:
            group = vals[1]
            station = None

        for src in self.map_sources:
            if group != src.get_name():
                continue

            if station:
                try:
                    point = src.get_point_by_name(station)
                except KeyError:
                    continue

                point.set_visible(vals[0])
                self.add_point_visible(point)
            else:
                src.set_visible(vals[0])
                for point in src.get_points():
                    point.set_visible(vals[0])
                    self.update_point(src, point)

            src.save()
            break
                
        self.map.queue_draw()

    def marker_mh(self, _action, id, group):
        action = _action.get_name()

        if action == "delete":
            print "Deleting %s/%s" % (group, id)
            for source in self.map_sources:
                if source.get_name() == group:
                    if not source.get_mutable():
                        return

                    point = source.get_point_by_name(id)
                    source.del_point(point)
                    source.save()
        elif action == "edit":
            for source in self.map_sources:
                if source.get_name() == group:
                    break

            if not source.get_mutable():
                return

            if not source:
                return

            for point in source.get_points():
                if point.get_name() == id:
                    break

            if not point:
                return

            _point = point.dup()
            upoint, foo = self.prompt_to_set_marker(point, source.get_name())
            if upoint:
                self.del_point(source, _point)
                self.add_point(source, upoint)
                source.save()

    def _make_marker_menu(self, store, iter):
        menu_xml = """
<ui>
  <popup name="menu">
    <menuitem action="edit"/>
    <menuitem action="delete"/>
    <menuitem action="center"/>
  </popup>
</ui>
"""
        ag = gtk.ActionGroup("menu")

        try:
            id, = store.get(iter, 1)
            group, = store.get(store.iter_parent(iter), 1)
        except TypeError:
            id = group = None

        edit = gtk.Action("edit", _("Edit"), None, None)
        edit.connect("activate", self.marker_mh, id, group)
        if not id:
            edit.set_sensitive(False)
        ag.add_action(edit)

        delete = gtk.Action("delete", _("Delete"), None, None)
        delete.connect("activate", self.marker_mh, id, group)
        ag.add_action(delete)

        center = gtk.Action("center", _("Center on this"), None, None)
        center.connect("activate", self.marker_mh, id, group)
        # This isn't implemented right now, because I'm lazy
        center.set_sensitive(False)
        ag.add_action(center)

        uim = gtk.UIManager()
        uim.insert_action_group(ag, 0)
        uim.add_ui_from_string(menu_xml)

        return uim.get_widget("/menu")

    def make_marker_popup(self, _, view, event):
        if event.button != 3:
            return

        if event.window == view.get_bin_window():
            x, y = event.get_coords()
            pathinfo = view.get_path_at_pos(int(x), int(y))
            if pathinfo is None:
                return
            else:
                view.set_cursor_on_cell(pathinfo[0])

        (store, iter) = view.get_selection().get_selected()

        menu = self._make_marker_menu(store, iter)
        if menu:
            menu.popup(None, None, None, event.button, event.time)

    def make_marker_list(self):
        cols = [(gobject.TYPE_BOOLEAN, _("Show")),
                (gobject.TYPE_STRING,  _("Station")),
                (gobject.TYPE_FLOAT,   _("Latitude")),
                (gobject.TYPE_FLOAT,   _("Longitude")),
                (gobject.TYPE_FLOAT,   _("Distance")),
                (gobject.TYPE_FLOAT,   _("Direction")),
                ]
        self.marker_list = miscwidgets.TreeWidget(cols, 1, parent=False)
        self.marker_list.toggle_cb.append(self.toggle_show)
        self.marker_list.connect("click-on-list", self.make_marker_popup)

        self.marker_list._view.connect("row-activated", self.recenter_cb)

        def render_station(col, rend, model, iter):
            parent = model.iter_parent(iter)
            if not parent:
                parent = iter
            group = model.get_value(parent, 1)
            if self.colors.has_key(group):
                rend.set_property("foreground", self.colors[group])

        c = self.marker_list._view.get_column(1)
        c.set_expand(True)
        c.set_min_width(150)
        r = c.get_cell_renderers()[0]
        c.set_cell_data_func(r, render_station)

        def render_coord(col, rend, model, iter, cnum):
            if model.iter_parent(iter):
                rend.set_property('text', "%.4f" % model.get_value(iter, cnum))
            else:
                rend.set_property('text', '')

        for col in [2, 3]:
            c = self.marker_list._view.get_column(col)
            r = c.get_cell_renderers()[0]
            c.set_cell_data_func(r, render_coord, col)

        def render_dist(col, rend, model, iter, cnum):
            if model.iter_parent(iter):
                rend.set_property('text', "%.2f" % model.get_value(iter, cnum))
            else:
                rend.set_property('text', '')

        for col in [4, 5]:
            c = self.marker_list._view.get_column(col)
            r = c.get_cell_renderers()[0]
            c.set_cell_data_func(r, render_dist, col)

        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        sw.add(self.marker_list.packable())
        sw.set_size_request(-1, 150)
        sw.show()

        return sw

    def refresh_marker_list(self, group=None):
        (lat, lon) = self.map.get_center()
        center = GPSPosition(lat=lat, lon=lon)

        for item in self.marker_list.get_values(group):
            try:
                _parent, children = item
            except ValueError:
                # Empty group
                continue

            parent = _parent[1]
            for child in children:
                this = GPSPosition(lat=child[2], lon=child[3])
                dist = center.distance_from(this)
                bear = center.bearing_to(this)

                self.marker_list.set_item(parent,
                                          child[0],
                                          child[1],
                                          child[2],
                                          child[3],
                                          dist,
                                          bear)

    def make_track(self):
        def toggle(cb, mw):
            mw.tracking_enabled = cb.get_active()

        cb = gtk.CheckButton(_("Track center"))
        cb.connect("toggled", toggle, self)

        cb.show()

        return cb

    def clear_map_cache(self):
        d = gtk.MessageDialog(buttons=gtk.BUTTONS_YES_NO)
        d.set_property("text", _("Are you sure you want to clear your map cache?"))
        r = d.run()
        d.destroy()

        if r == gtk.RESPONSE_YES:
            dir = os.path.join(platform.get_platform().config_dir(), "maps")
            shutil.rmtree(dir, True)
            self.map.queue_draw()
        
    def printable_map(self, bounds=None):
        p = platform.get_platform()

        f = tempfile.NamedTemporaryFile()
        fn = f.name
        f.close()

        mf = "%s.png" % fn
        hf = "%s.html" % fn

        ts = time.strftime("%H:%M:%S %d-%b-%Y")

        station_map = _("Station map")
        generated_at = _("Generated at")

        html = """
<html>
<body>
<h2>D-RATS %s</h2>
<h5>%s %s</h5>
<img src="file://%s"/>
</body>
</html>
""" % (station_map, generated_at, ts, mf)

        self.map.export_to(mf, bounds)

        f = file(hf, "w")
        f.write(html)
        f.close()

        p.open_html_file(hf)        

    def save_map(self, bounds=None):
        p = platform.get_platform()
        f = p.gui_save_file(default_name="map_%s.png" % \
                                time.strftime("%m%d%Y%_H%M%S"))
        if not f:
            return

        if not f.endswith(".png"):
            f += ".png"
        self.map.export_to(f, bounds)

    def get_visible_bounds(self):
        ha = self.sw.get_hadjustment()
        va = self.sw.get_vadjustment()

        return (int(ha.value), int(va.value),
                int(ha.value + ha.page_size), int(va.value + va.page_size))

    def mh(self, _action):
        action = _action.get_name()

        if action == "refresh":
            self.map_tiles = []
            self.map.queue_draw()
        elif action == "clearcache":
            self.clear_map_cache()
        elif action == "save":
            self.save_map()
        elif action == "savevis":
            self.save_map(self.get_visible_bounds())
        elif action == "printable":
            self.printable_map()
        elif action == "printablevis":
            self.printable_map(self.get_visible_bounds())
        elif action == "editsources":
            srced = map_source_editor.MapSourcesEditor(self.config)
            srced.run()
            srced.destroy()
            self.emit("reload-sources")

    def make_menu(self):
        menu_xml = """
<ui>
  <menubar name="MenuBar">
    <menu action="map">
      <menuitem action="refresh"/>
      <menuitem action="clearcache"/>
      <menuitem action="editsources"/>
      <menu action="export">
        <menuitem action="printable"/>
        <menuitem action="printablevis"/>
        <menuitem action="save"/>
        <menuitem action="savevis"/>
      </menu>
    </menu>
  </menubar>
</ui>
"""

        actions = [('map', None, "_" + _("Map"), None, None, self.mh),
                   ('refresh', None, "_" + _("Refresh"), None, None, self.mh),
                   ('clearcache', None, "_" + _("Clear Cache"), None, None, self.mh),
                   ('editsources', None, _("Edit Sources"), None, None, self.mh),
                   ('export', None, "_" + _("Export"), None, None, self.mh),
                   ('printable', None, "_" + _("Printable"), "<Control>p", None, self.mh),
                   ('printablevis', None, _("Printable (visible area)"), "<Control><Alt>P", None, self.mh),
                   ('save', None, "_" + _("Save Image"), "<Control>s", None, self.mh),
                   ('savevis', None, _('Save Image (visible area)'), "<Control><Alt>S", None, self.mh),
                   ]

        uim = gtk.UIManager()
        self.menu_ag = gtk.ActionGroup("MenuBar")

        self.menu_ag.add_actions(actions)
        
        uim.insert_action_group(self.menu_ag, 0)
        menuid = uim.add_ui_from_string(menu_xml)

        self._accel_group = uim.get_accel_group()

        return uim.get_widget("/MenuBar")

    def make_controls(self):
        vbox = gtk.VBox(False, 2)

        vbox.pack_start(self.make_zoom_controls(), 0,0,0)
        vbox.pack_start(self.make_track(), 0,0,0)

        vbox.show()

        return vbox

    def make_bottom_pane(self):
        box = gtk.HBox(False, 2)

        box.pack_start(self.make_marker_list(), 1,1,1)
        box.pack_start(self.make_controls(), 0,0,0)

        box.show()

        return box

    def scroll_to_center(self, widget):
        a = widget.get_vadjustment()
        a.set_value((a.upper - a.page_size) / 2)

        a = widget.get_hadjustment()
        a.set_value((a.upper - a.page_size) / 2)

    def center_on(self, lat, lon):
        ha = self.sw.get_hadjustment()
        va = self.sw.get_vadjustment()

        x, y = self.map.latlon2xy(lat, lon)

        ha.set_value(x - (ha.page_size / 2))
        va.set_value(y - (va.page_size / 2))

    def status(self, frac, message):
        self.sb_prog.set_text(message)
        self.sb_prog.set_fraction(frac)

    def recenter(self, lat, lon):
        self.map.set_center(lat, lon)
        self.map.load_tiles()
        self.refresh_marker_list()
        self.center_on(lat, lon)
        self.map.queue_draw()

    def refresh(self):
        self.map.load_tiles()

    def prompt_to_set_marker(self, point, group=None):
        def do_address(button, latw, lonw, namew):
            dlg = geocode_ui.AddressAssistant()
            r = dlg.run()
            if r == gtk.RESPONSE_OK:
                if not namew.get_text():
                    namew.set_text(dlg.place)
                latw.set_text("%.5f" % dlg.lat)
                lonw.set_text("%.5f" % dlg.lon)

        d = MarkerEditDialog()

        sources = []
        for src in self.map_sources:
            if src.get_mutable():
                sources.append(src.get_name())

        d.set_groups(sources, group)
        d.set_point(point)
        r = d.run()
        if r == gtk.RESPONSE_OK:
            point = d.get_point()
            group = d.get_group()
        d.destroy()

        if r == gtk.RESPONSE_OK:
            return point, group
        else:
            return None, None

    def prompt_to_send_loc(self, _lat, _lon):
        d = inputdialog.FieldDialog(title=_("Broadcast Location"))

        d.add_field(_("Callsign"), gtk.Entry(8))
        d.add_field(_("Description"), gtk.Entry(20))
        d.add_field(_("Latitude"), miscwidgets.LatLonEntry())
        d.add_field(_("Longitude"), miscwidgets.LatLonEntry())
        d.get_field(_("Latitude")).set_text("%.4f" % _lat)
        d.get_field(_("Longitude")).set_text("%.4f" % _lon)

        while d.run() == gtk.RESPONSE_OK:
            try:
                call = d.get_field(_("Callsign")).get_text()
                desc = d.get_field(_("Description")).get_text()
                lat = d.get_field(_("Latitude")).get_text()
                lon = d.get_field(_("Longitude")).get_text()

                fix = GPSPosition(lat=lat, lon=lon, station=call)
                fix.comment = desc

                for port in self.emit("get-station-list").keys():
                    self.emit("user-send-chat",
                              "CQCQCQ", port,
                              fix.to_NMEA_GGA(), True)
                    
                break
            except Exception, e:
                utils.log_exception()
                ed = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, parent=d)
                ed.set_property("text", _("Invalid value") + ": %s" % e)
                ed.run()
                ed.destroy()

        d.destroy()

    def recenter_cb(self, view, path, column, data=None):
        model = view.get_model()
        if model.iter_parent(model.get_iter(path)) == None:
            return

        items = self.marker_list.get_selected()

        self.center_mark = items[1]
        self.recenter(items[2], items[3])

        self.sb_center.pop(self.STATUS_CENTER)
        self.sb_center.push(self.STATUS_CENTER, _("Center") + ": %s" % self.center_mark)

    def make_popup(self, vals):
        def _an(cap):
            return cap.replace(" ", "_")

        xml = ""
        for action in [_an(x) for x in self._popup_items.keys()]:
            xml += "<menuitem action='%s'/>\n" % action

        xml = """
<ui>
  <popup name="menu">
    <menuitem action='title'/>
    <separator/>
    %s
  </popup>
</ui>
""" % xml
        ag = gtk.ActionGroup("menu")

        t = gtk.Action("title",
                       "%.4f,%.4f" % (vals["lat"], vals["lon"]),
                       None,
                       None)
        t.set_sensitive(False)
        ag.add_action(t)

        for name, handler in self._popup_items.items():
            action = gtk.Action(_an(name), name, None, None)
            action.connect("activate", handler, vals)
            ag.add_action(action)

        uim = gtk.UIManager()
        uim.insert_action_group(ag, 0)
        uim.add_ui_from_string(xml)

        return uim.get_widget("/menu")

    def mouse_click_event(self, widget, event):
        x,y = event.get_coords()

        ha = widget.get_hadjustment()
        va = widget.get_vadjustment()
        mx = x + int(ha.get_value())
        my = y + int(va.get_value())

        lat, lon = self.map.xy2latlon(mx, my)

        print "Button %i at %i,%i" % (event.button, mx, my)
        if event.button == 3:
            vals = { "lat" : lat,
                     "lon" : lon,
                     "x" : mx,
                     "y" : my }
            menu = self.make_popup(vals)
            if menu:
                menu.popup(None, None, None, event.button, event.time)
        elif event.type == gtk.gdk.BUTTON_PRESS:
            print "Clicked: %.4f,%.4f" % (lat, lon)
            # The crosshair marker has been missing since 0.3.0
            #self.set_marker(GPSPosition(station=CROSSHAIR,
            #                            lat=lat, lon=lon))
        elif event.type == gtk.gdk._2BUTTON_PRESS:
            print "Recenter on %.4f, %.4f" % (lat,lon)

            self.recenter(lat, lon)

    def mouse_move_event(self, widget, event):
        if not self.__last_motion:
            gobject.timeout_add(100, self._mouse_motion_handler)
        self.__last_motion = (time.time(), event.x, event.y)

    def _mouse_motion_handler(self):
        if self.__last_motion == None:
            return False

        t, x, y = self.__last_motion
        if (time.time() - t) < 0.5:
            self.info_window.hide()
            return True

        lat, lon = self.map.xy2latlon(x, y)

        ha = self.sw.get_hadjustment()
        va = self.sw.get_vadjustment()
        mx = x - int(ha.get_value())
        my = y - int(va.get_value())

        hit = False

        for source in self.map_sources:
            if not source.get_visible():
                continue
            for point in source.get_points():
                if not point.get_visible():
                    continue
                try:
                    _x, _y = self.map.latlon2xy(point.get_latitude(),
                                                point.get_longitude())
                except ZeroDivisionError:
                    continue

                dx = abs(x - _x)
                dy = abs(y - _y)

                if dx < 20 and dy < 20:
                    hit = True

                    date = time.ctime(point.get_timestamp())

                    text = "<b>Station:</b> %s" % point.get_name() + \
                        "\n<b>Latitude:</b> %.5f" % point.get_latitude() + \
                        "\n<b>Longitude:</b> %.5f"% point.get_longitude() + \
                        "\n<b>Last update:</b> %s" % date

                    text += "\n<b>Info</b>: %s" % point.get_comment()

                    label = gtk.Label()
                    label.set_markup(text)
                    label.show()
                    for child in self.info_window.get_children():
                        self.info_window.remove(child)
                    self.info_window.add(label)
                    
                    posx, posy = self.get_position()
                    posx += mx + 10
                    posy += my - 10

                    self.info_window.move(int(posx), int(posy))
                    self.info_window.show()

                    break


        if not hit:
            self.info_window.hide()

        self.sb_coords.pop(self.STATUS_COORD)
        self.sb_coords.push(self.STATUS_COORD, "%.4f, %.4f" % (lat, lon))

        self.__last_motion = None

        return False

    def ev_destroy(self, widget, data=None):
        self.hide()
        return True

    def ev_delete(self, widget, event, data=None):
        self.hide()
        return True

    def update_gps_status(self, string):
        self.sb_gps.pop(self.STATUS_GPS)
        self.sb_gps.push(self.STATUS_GPS, string)

    def add_point_visible(self, point):
        if point in self.points_visible:
            self.points_visible.remove(point)

        if self.map.point_is_visible(point.get_latitude(),
                                     point.get_longitude()):
            if point.get_visible():
                self.points_visible.append(point)
                return True
            else:
                return False
        else:
            return False

    def update_point(self, source, point):
        (lat, lon) = self.map.get_center()
        center = GPSPosition(*self.map.get_center())
        this = GPSPosition(point.get_latitude(), point.get_longitude())

        try:
            self.marker_list.set_item(source.get_name(),
                                      point.get_visible(),
                                      point.get_name(),
                                      point.get_latitude(),
                                      point.get_longitude(),
                                      center.distance_from(this),
                                      center.bearing_to(this))
        except Exception, e:
            if str(e) == "Item not found":
                # this is evil
                print "Adding point instead of updating"
                return self.add_point(source, point)
                
        self.add_point_visible(point)
        self.map.queue_draw()

    def add_point(self, source, point):
        (lat, lon) = self.map.get_center()
        center = GPSPosition(*self.map.get_center())
        this = GPSPosition(point.get_latitude(), point.get_longitude())

        self.marker_list.add_item(source.get_name(),
                                  point.get_visible(), point.get_name(),
                                  point.get_latitude(),
                                  point.get_longitude(),
                                  center.distance_from(this),
                                  center.bearing_to(this))
        self.add_point_visible(point)
        self.map.queue_draw()

    def del_point(self, source, point):
        self.marker_list.del_item(source.get_name(), point.get_name())

        if point in self.points_visible:
            self.points_visible.remove(point)

        self.map.queue_draw()

    def get_map_source(self, name):
        for source in self.get_map_sources():
            if source.get_name() == name:
                return source
        return None

    def add_map_source(self, source):
        self.map_sources.append(source)
        self.marker_list.add_item(None,
                                  source.get_visible(), source.get_name(),
                                  0, 0, 0, 0)
        for point in source.get_points():
            self.add_point(source, point)

        #source.connect("point-updated", self.update_point)
        source.connect("point-added", self.add_point)
        source.connect("point-deleted", self.del_point)
        source.connect("point-updated", self.maybe_recenter_on_updated_point)

    def update_points_visible(self):
        for src in self.map_sources:
            for point in src.get_points():
                self.update_point(src, point)

        self.map.queue_draw()

    def maybe_recenter_on_updated_point(self, source, point):
        if point.get_name() == self.center_mark and \
                self.tracking_enabled:
            print "Center updated"
            self.recenter(point.get_latitude(), point.get_longitude())
        self.update_point(source, point)

    def clear_map_sources(self):
        self.marker_list.clear()
        self.map_sources = []
        self.points_visible = []
        self.update_points_visible()

    def get_map_sources(self):
        return self.map_sources

    def redraw_markers(self, map):
        for point in self.points_visible:
            map.draw_marker(point.get_name(),
                            point.get_latitude(),
                            point.get_longitude(),
                            point.get_icon())

    def __init__(self, config, *args):
        gtk.Window.__init__(self, *args)

        self.config = config

        self.STATUS_COORD = 0
        self.STATUS_CENTER = 1
        self.STATUS_GPS = 2

        self.center_mark = None
        self.tracking_enabled = False

        tiles = 5

        self.points_visible = []
        self.map_sources = []
        self.map = MapWidget(tiles, tiles, status=self.status)
        self.map.show()
        self.map.connect("redraw-markers", self.redraw_markers)
        self.map.connect("new-tiles-loaded", 
                         lambda m: self.update_points_visible())

        box = gtk.VBox(False, 2)

        self.menubar = self.make_menu()
        self.menubar.show()
        box.pack_start(self.menubar, 0,0,0)
        self.add_accel_group(self._accel_group)

        self.sw = gtk.ScrolledWindow()
        self.sw.add_with_viewport(self.map)
        self.sw.show()


        def pre_scale(sw, event, mw):
            ha = mw.sw.get_hadjustment()
            va = mw.sw.get_vadjustment()

            px = ha.get_value() + ha.page_size
            py = va.get_value() + va.page_size

            rect = gtk.gdk.Rectangle(int(ha.get_value()), int(va.get_value()),
                                     int(py), int(py))
            mw.map.window.invalidate_rect(rect, True)

        @utils.run_gtk_locked
        def _scale(sw, event, mw):
            ha = mw.sw.get_hadjustment()
            va = mw.sw.get_vadjustment()

            px = ha.get_value() + ha.page_size
            py = va.get_value() + va.page_size

            pm = mw.map.scale(int(px) - 5, int(py))

        def scale(sw, event, mw):
            gobject.idle_add(_scale, sw, event, mw)

        self.sw.connect("expose-event", pre_scale, self)
        self.sw.connect_after("expose-event", scale, self)

        self.__last_motion = None

        self.map.add_events(gtk.gdk.POINTER_MOTION_MASK)
        self.map.connect("motion-notify-event", self.mouse_move_event)
        self.sw.connect("button-press-event", self.mouse_click_event)

        self.sw.connect('realize', self.scroll_to_center)

        hbox = gtk.HBox(False, 2)

        self.sb_coords = gtk.Statusbar()
        self.sb_coords.show()
        self.sb_coords.set_has_resize_grip(False)

        self.sb_center = gtk.Statusbar()
        self.sb_center.show()
        self.sb_center.set_has_resize_grip(False)

        self.sb_gps = gtk.Statusbar()
        self.sb_gps.show()

        self.sb_prog = gtk.ProgressBar()
        self.sb_prog.set_size_request(150, -1)
        self.sb_prog.show()

        hbox.pack_start(self.sb_coords, 1,1,1)
        hbox.pack_start(self.sb_center, 1,1,1)
        hbox.pack_start(self.sb_prog, 0,0,0)
        hbox.pack_start(self.sb_gps, 1,1,1)
        hbox.show()

        box.pack_start(self.sw, 1,1,1)
        box.pack_start(self.make_bottom_pane(), 0,0,0)
        box.pack_start(hbox, 0,0,0)
        box.show()

        self.set_default_size(600,600)
        self.set_geometry_hints(max_width=tiles*256,
                                max_height=tiles*256)

        self.markers = {}
        self.colors = {}
        self.color_index = 0

        self.add(box)

        self.connect("destroy", self.ev_destroy)
        self.connect("delete_event", self.ev_delete)

        self._popup_items = {}

        self.add_popup_handler(_("Center here"),
                               lambda a, vals:
                                   self.recenter(vals["lat"],
                                                 vals["lon"]))

        def set_mark_at(a, vals):
            p = map_sources.MapStation("STATION", vals["lat"], vals["lon"])
            p.set_icon_from_aprs_sym("\\<")
            point, group = self.prompt_to_set_marker(p)
            if not point:
                return

            for source in self.map_sources:
                print "%s,%s" % (source.get_name(), group)
                if source.get_name() == group:
                    print "Adding new point %s to %s" % (point.get_name(),
                                                         source.get_name())
                    source.add_point(point)
                    source.save()
                    return
            # No matching group
            q = "%s %s %s" % \
                (_("Group"), group,
                 _("does not exist.  Do you want to create it?"))
            if not ask_for_confirmation(q):
                return
                                            
            s = map_sources.MapFileSource.open_source_by_name(self.config,
                                                              group,
                                                              True)
            s.add_point(point)
            s.save()
            self.add_map_source(s)

        self.add_popup_handler(_("New marker here"), set_mark_at)

        self.add_popup_handler(_("Broadcast this location"),
                               lambda a, vals:
                                   self.prompt_to_send_loc(vals["lat"],
                                                           vals["lon"]))

        self.info_window = gtk.Window(gtk.WINDOW_POPUP)
        self.info_window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_MENU)
        self.info_window.set_decorated(False)
        self.info_window.modify_bg(gtk.STATE_NORMAL,
                                   gtk.gdk.color_parse("yellow"))

    def add_popup_handler(self, name, handler):
        self._popup_items[name] = handler

    def set_zoom(self, zoom):
        self.map.set_zoom(zoom)

    def set_center(self, lat, lon):
        self.map.set_center(lat, lon)

if __name__ == "__main__":

    import sys
    import gps

    if len(sys.argv) == 3:
        m = MapWindow()
        m.set_center(gps.parse_dms(sys.argv[1]),
                     gps.parse_dms(sys.argv[2]))
        m.set_zoom(15)
    else:
        m = MapWindow()
        m.set_center(45.525012, -122.916434)
        m.set_zoom(14)

        m.set_marker(GPSPosition(station="KI4IFW_H", lat=45.520, lon=-122.916434))
        m.set_marker(GPSPosition(station="KE7FTE", lat=45.5363, lon=-122.9105))
        m.set_marker(GPSPosition(station="KA7VQH", lat=45.4846, lon=-122.8278))
        m.set_marker(GPSPosition(station="N7QQU", lat=45.5625, lon=-122.8645))
        m.del_marker("N7QQU")


    m.show()

    try:
        gtk.main()
    except:
        pass


#    area = gtk.DrawingArea()
#    area.set_size_request(768, 768)
#
#    w = gtk.Window(gtk.WINDOW_TOPLEVEL)
#    w.add(area)
#    area.show()
#    w.show()
#
#    def expose(area, event):
#        for i in range(1,4):
#            img = gtk.gdk.pixbuf_new_from_file("/tmp/tile%i.png" % i)
#            area.window.draw_pixbuf(area.get_style().black_gc,
#                                    img,
#                                    0, 0, 256 * (i-1), 0, 256, 256)
#
#    area.connect("expose-event", expose)
#

Generated by  Doxygen 1.6.0   Back to index