[Thuban-commits] r2861 - in trunk/thuban: Extensions Extensions/gMapTiles Extensions/gMapTiles/sample Extensions/gMapTiles/test Thuban

scm-commit@wald.intevation.org scm-commit at wald.intevation.org
Wed Jul 30 07:52:31 CEST 2008


Author: elachuni
Date: 2008-07-30 07:52:31 +0200 (Wed, 30 Jul 2008)
New Revision: 2861

Added:
   trunk/thuban/Extensions/gMapTiles/
   trunk/thuban/Extensions/gMapTiles/README.txt
   trunk/thuban/Extensions/gMapTiles/__init__.py
   trunk/thuban/Extensions/gMapTiles/exportGMapTiles.py
   trunk/thuban/Extensions/gMapTiles/exporter.py
   trunk/thuban/Extensions/gMapTiles/sample/
   trunk/thuban/Extensions/gMapTiles/sample/README.txt
   trunk/thuban/Extensions/gMapTiles/sample/sample.html
   trunk/thuban/Extensions/gMapTiles/sample/tile.py
   trunk/thuban/Extensions/gMapTiles/test/
   trunk/thuban/Extensions/gMapTiles/test/test_gMapTileExport.py
Modified:
   trunk/thuban/Thuban/thuban_cfg.py
Log:
First version of the Google Maps Tiles (exporter) extension.



Added: trunk/thuban/Extensions/gMapTiles/README.txt
===================================================================
--- trunk/thuban/Extensions/gMapTiles/README.txt	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/README.txt	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,10 @@
+This extension generates 256x256px png tiles out of Thuban maps, to be able to
+overlay the data on Google Maps.
+
+The extension reprojects and chops the tiles accordingly so that the data
+matches Google's imagery.
+
+For now the extension only generates the tiles, you'll need to write your
+own javascript to use them.  Check the "sample/" subdirectory for a very simple
+way of viewing the data you generate.
+

Added: trunk/thuban/Extensions/gMapTiles/__init__.py
===================================================================
--- trunk/thuban/Extensions/gMapTiles/__init__.py	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/__init__.py	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,22 @@
+# Copyright (c) 2008 by Intevation GmbH
+# Authors:
+# Anthony Lenton <anthony at except.com.ar>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with Thuban for details.
+
+# import the actual module
+import exportGMapTiles
+
+# perform the registration of the extension
+from Thuban import _
+from Thuban.UI.extensionregistry import ExtensionDesc, ext_registry
+
+ext_registry.add(ExtensionDesc(
+    name = 'exportGMapTiles',
+    version = '0.1.1',
+    authors= [ 'Anthony Lenton' ],
+    copyright = '2008 by Intevation GmbH',
+    desc = _("Export PNG tiles to be loaded with Google Maps\n" \
+             "as a GTileLayerOverlay.")))
+

Added: trunk/thuban/Extensions/gMapTiles/exportGMapTiles.py
===================================================================
--- trunk/thuban/Extensions/gMapTiles/exportGMapTiles.py	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/exportGMapTiles.py	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,134 @@
+# Copyright (c) 2008 by Intevation GmbH
+# Authors:
+# Anthony Lenton <anthony at except.com.ar>
+#
+# This program is free software under the GPL (>=v2)
+# Read the file COPYING coming with Thuban for details.
+
+""" This module allows you to save your data as png files to be served as a
+    Google Maps layer overlay. """
+
+import os
+import wx
+
+if __name__ != '__main__':
+    from Thuban.UI.command import registry, Command
+    from Thuban import _
+    from Thuban.UI.mainwindow import main_menu
+    from Thuban.UI import internal_from_wxstring
+
+from exporter import export
+
+DEFAULT_DESTINATION_FOLDER = os.path.join (os.getcwd(), "tiles")
+
+class exportGMapTilesDialog(wx.Dialog):
+    def __init__(self, parent, ID, title,
+                 pos=wx.DefaultPosition, size=wx.DefaultSize,
+                 style=wx.DEFAULT_DIALOG_STYLE):
+                     
+        # initialize the Dialog
+        wx.Dialog.__init__(self, parent, ID, title, pos, size, style)
+
+        # Destination folder
+        box_folder = wx.BoxSizer(wx.HORIZONTAL)
+        box_folder.Add(wx.StaticText(self, -1, _("Destination folder:")), 1,
+                     wx.ALL|wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL, 4)
+        self.text_folder = wx.TextCtrl(self, -1, DEFAULT_DESTINATION_FOLDER)
+        box_folder.Add(self.text_folder, 2, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
+        self.button_folder = wx.Button (self, -1, _("Choose folder"))
+        self.button_folder.Bind (wx.EVT_BUTTON, self.OnChooseFolder)
+        box_folder.Add(self.button_folder, 1,wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
+
+        # Zoom levels
+        box_zoom = wx.BoxSizer (wx.HORIZONTAL)
+        box_zoom.Add (wx.StaticText (self, -1, _("Minimum Zoom:")), 0,
+                      wx.ALL|wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL, 4)
+        self.minZoomSpin = wx.SpinCtrl (self, size=(60, -1), min=1, max=17)
+        self.Bind(wx.EVT_SPINCTRL, self.OnMinZoomChanged, self.minZoomSpin)
+        box_zoom.Add (self.minZoomSpin, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
+        box_zoom.Add (wx.StaticText (self, -1, _("Maximum Zoom:")), 0,
+                      wx.ALL|wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL, 4)
+        self.maxZoomSpin = wx.SpinCtrl (self, size=(60, -1), min=1, max=17)
+        self.Bind(wx.EVT_SPINCTRL, self.OnMaxZoomChanged, self.maxZoomSpin)
+        box_zoom.Add (self.maxZoomSpin, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
+
+        #buttons
+        box_buttons = wx.BoxSizer(wx.HORIZONTAL)
+        button = wx.Button(self, wx.ID_OK, _("OK"))
+        box_buttons.Add(button, 0, wx.ALL, 5)
+        button = wx.Button(self, wx.ID_CANCEL, _("Cancel"))
+        box_buttons.Add(button, 0, wx.ALL, 5)
+        #set the button funcitons
+        self.Bind(wx.EVT_BUTTON, self.OnOK, id=wx.ID_OK)
+        self.Bind(wx.EVT_BUTTON, self.OnCancel, id=wx.ID_CANCEL)
+      
+        # compose the final dialog
+        top = wx.BoxSizer(wx.VERTICAL)
+        top.Add(box_folder, 0, wx.EXPAND |wx.ALL, 5)
+        top.Add(box_zoom, 0, wx.EXPAND |wx.ALL, 5)
+        top.Add(box_buttons, 0, wx.ALIGN_RIGHT)
+        
+        # final layout settings
+        self.SetSizer(top)
+        top.Fit(self)
+
+    def OnMinZoomChanged (self, event):
+        if self.maxZoomSpin.GetValue() < self.minZoomSpin.GetValue():
+            self.maxZoomSpin.SetValue(self.minZoomSpin.GetValue())
+
+    def OnMaxZoomChanged(self, event):
+        if self.minZoomSpin.GetValue() > self.maxZoomSpin.GetValue():
+            self.minZoomSpin.SetValue(self.maxZoomSpin.GetValue())
+
+    def OnChooseFolder (self, event):
+        dlg = wx.DirDialog(self, _("Select a folder to save the tiles in"), ".")
+        if dlg.ShowModal() == wx.ID_OK:
+            pfm_filename = internal_from_wxstring(dlg.GetPath())
+            self.text_folder.ChangeValue (pfm_filename)
+        dlg.Destroy()
+
+    def RunDialog(self):
+        self.ShowModal()
+        self.Destroy()
+
+    def endDialog(self, result):
+        self.result = result
+        if self.result is not None:
+            self.EndModal(wx.ID_OK)
+        else:
+            self.EndModal(wx.ID_CANCEL)
+        self.Show(False)
+
+    def OnOK(self, event):
+        self.result = "OK"
+        self.dataFolder = self.text_folder.GetValue()
+        self.minZoom = self.minZoomSpin.GetValue()
+        self.maxZoom = self.maxZoomSpin.GetValue()
+        self.endDialog(self.result)
+
+    def OnCancel(self, event):
+        self.endDialog(None)
+
+
+def export_gmap_dialog(context):
+    """ Select the destination folder and zoom levels needed.
+
+    context -- The Thuban context.
+    """
+    dlg = exportGMapTilesDialog(context.mainwindow, -1,
+                       _("Export Google Maps tiles"))
+    dlg.RunDialog()
+    if dlg.result == "OK":
+        export (context, dlg.minZoom, dlg.maxZoom, dlg.dataFolder)
+
+# register the new command
+registry.Add(Command('export-gmaptiles', _("(experimental) ") + _('Export GMap Tiles'),
+                     export_gmap_dialog, helptext=_('Export as Google Map tiles')))
+
+# find the extension menu (create it anew if not found)
+extensions_menu = main_menu.FindOrInsertMenu('extensions',
+                                               _('E&xtensions'))
+
+# finally add the new entry to the menu
+extensions_menu.InsertItem('export-gmaptiles')
+

Added: trunk/thuban/Extensions/gMapTiles/exporter.py
===================================================================
--- trunk/thuban/Extensions/gMapTiles/exporter.py	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/exporter.py	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,161 @@
+import os
+import math
+import mapscript
+from Extensions.umn_mapserver.mf_export import thuban_to_map
+from Extensions.umn_mapserver.mapfile import MF_Map
+from Thuban.Model.proj import Projection
+try:
+    from PythonMagick import Image
+    pythonMagick = True
+except:
+    print "Coultn't import PythonMagick.  gMapTile will call convert instead."
+    pythonMagick = False
+
+cmd = 'convert -crop 256x256+%d+%d %s/%d/model.png %s/%d/%d.%d.png'
+
+T = 2048 # Big Tile size
+
+# Port to Python of Google Map's javascript GMercatorClass
+class Mercator (object):
+    def __init__(self, zoom):
+        self.tileSize = 256
+        self.zoom = zoom
+        self.tiles = 2 ** zoom
+        self.circumference = self.tileSize * self.tiles
+        self.semi = self.circumference / 2
+        self.radius = self.circumference / (2 * math.pi)
+        self.fE = -1.0 * self.circumference / 2.0
+        self.fN = self.circumference / 2.0 
+
+    def longToX (self, degrees):
+        longitude = math.radians(degrees + 180)
+        return (self.radius * longitude)
+
+    def latToY (self, degrees):
+        latitude = math.radians(degrees)
+        y = self.radius/2.0 * math.log((1.0 + math.sin(latitude)) /
+                                           (1.0 - math.sin(latitude)))
+        return self.fN - y
+
+    def xToLong (self, x):
+        longRadians = x/self.radius
+        longDegrees = math.degrees(longRadians) - 180
+        rotations = math.floor((longDegrees + 180)/360)
+        longitude = longDegrees - (rotations * 360)
+        return longitude
+
+    def yToLat (self, y):
+        y = self.fN - y
+        latitude = ((math.pi / 2) -
+                    (2 * math.atan(math.exp(-1.0 * y / self.radius))))
+        return math.degrees(latitude)
+
+def generate (mf, pixtents, zoom, folder):
+    """ Generate an image of the map, and then
+        Chop up in to 256x256px tiles.  'mf' is the mapObj object completely
+        configured except for its size and extents.
+        'pixtents' is the extents to be shown, in projected (pixel)
+        coordinates.
+        'zoom' is the zoom level for the image.
+        'folder' is the folder to save the tiles. """
+    c = Mercator(zoom)
+    extents = [pixtents[0] - c.semi, c.semi - pixtents[1],
+               pixtents[2] - c.semi, c.semi - pixtents[3]]
+    mf.set_size (pixtents[2] - pixtents[0], pixtents[1] - pixtents[3])
+    mf.set_extent (extents)
+    img = mf._mf_map.draw()
+    png = img.save(folder+'/%d/model.png' % (zoom,))
+    xTile = pixtents[0] // 256
+    yTile = pixtents[3] // 256
+    width = pixtents[2] - pixtents[0]
+    height = pixtents[1] - pixtents[3]
+    i = xTile
+    x = 0
+    if pythonMagick:
+        img = Image(str(folder+"/%d/model.png" % (zoom,)))
+    while x < width:
+        y = 0
+        j = yTile+1
+        while y < height:
+            if pythonMagick:
+                img2 = Image(img)
+                img2.crop ('256x256+%d+%d' % (x, y))
+                img2.write (str(folder+"/%d/%d.%d.png" % (zoom, i, j)))
+            else:
+                c = cmd % (x, y, folder, zoom, folder, zoom, i, j)
+                os.system(c)
+            y += 256
+            j += 1
+        x += 256
+        i += 1
+
+def export (context, minZoom, maxZoom, dataFolder, filename=None):
+    """ Use the Thuban context to render the current map to gMapTiles.
+        All zoom levels between 'minZoom' and 'maxZoom' will be generated.
+        Tiles will be saved to 'dataFolder'.
+        If 'filename' is not None, the map will be generated from this mapfile,
+        instead of using 'context' (for testing only). """
+    if filename is None:
+        mf = MF_Map(mapscript.mapObj(""))
+        mf.set_size (60, 50) # Set some size for thuban_to_map not to fail
+        thuban_to_map (context, mf)
+    else:
+        mf = MF_Map(mapscript.mapObj(filename))
+    # We need all layers to have a projection, assume that they're
+    # using un-projected (lat/long) coordinates.  Probably an exception
+    # should be raised instead
+    for lindex in range(mf._mf_map.numlayers):
+        l = mf._mf_map.getLayer(lindex)
+        if l.getProjection() == '':
+            l.setProjection("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
+    mf.set_imagetype ('png')
+    mf._mf_map.imagecolor.setRGB(234, 234, 234)
+    mf._mf_map.transparent = 1
+    of = mapscript.outputFormatObj("GD/png")
+    of.transparent = 1
+    mf._mf_map.setOutputFormat (of)
+
+    if filename is None:
+        extents = context.mainwindow.canvas.map.BoundingBox() # Unprojected
+    else:
+        extents = mf.get_extent().get_rect()
+    if extents is None:
+        return
+    minx, miny, maxx, maxy = extents
+    for zoom in range(minZoom, maxZoom + 1):
+        try:
+            os.makedirs (os.path.join(dataFolder, str(zoom)))
+        except OSError:
+            pass # Lets assume that the directory already exists
+        coord = Mercator(zoom)
+        gMercator = ["proj=merc", "a=%f"%coord.radius, "b=%f"%coord.radius]
+        mf.set_projection (Projection(gMercator))
+        mminx = ((int(coord.longToX(minx))) // 256) * 256 # Projected (pixels)
+        mmaxx = ((int(coord.longToX(maxx))) // 256) * 256
+        mmaxy = ((int(coord.latToY(maxy)) + 255)// 256) * 256 - 1
+        mminy = ((int(coord.latToY(miny)) + 255)// 256) * 256 - 1
+        width = mmaxx - mminx
+        height = mminy - mmaxy
+        width = max (width, 1)
+        height = max (height, 1)
+        if width <= T and height < T:
+            pixtents = [mminx, mminy, mmaxx, mmaxy]
+            generate (mf, pixtents, zoom, dataFolder)
+        else:
+            x = mminx
+            while x < mmaxx:
+                y = mmaxy
+                while y < mminy:
+                    pixtents = [x, min(y + T - 1, mminy), min(x + T - 1, mmaxx), y]
+                    generate (mf, pixtents, zoom, dataFolder)
+                    y += T
+                x += T
+
+if __name__ == '__main__':
+    import sys
+    if len(sys.argv) <= 1:
+        print "-----\nUsage: python exporter.py <mapfile.map>"
+    else:
+        # Say, let's export only for zoom level seven.  I like seven.
+        export (None, 7, 7, 'tiles', sys.argv[-1])
+

Added: trunk/thuban/Extensions/gMapTiles/sample/README.txt
===================================================================
--- trunk/thuban/Extensions/gMapTiles/sample/README.txt	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/sample/README.txt	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,20 @@
+This is a sample application of the gMapTiles extension.  To test this sample
+you need to:
+
+ * Use thuban to generate tiles for some data.
+
+ * Make tile.py accessible via web on your web server, and make sure that it has
+   execution permissions.  Let's say that the URL to tile.py is now
+   http://yourserver/tile.py.  If you open this URL in a web browser you should
+   see the message "Missing input arguments"
+
+ * Edit tile.py and replace the PATH_TO_TILES string constant for the absolute
+   path where you saved the tiles.
+
+ * Make sample.html accessible via web on your web server.  Edit sample.html
+   to use your own Google Maps API Key, and replace the URL_TO_TILE_PY string
+   constant with tile.py's absolute URL. (make sure your key is valid for the
+   sample's URL, more info, http://code.google.com/apis/maps/documentation/ )
+
+Enjoy!
+

Added: trunk/thuban/Extensions/gMapTiles/sample/sample.html
===================================================================
--- trunk/thuban/Extensions/gMapTiles/sample/sample.html	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/sample/sample.html	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
+<head>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+  <title>Google Map Tiles Proof of concept</title>
+  <script src="http://maps.google.com/maps?file=api&amp;v=2.x&amp;key=[Insert your Google Maps API Key here]"
+          type="text/javascript">
+  </script>
+  <script type="text/javascript">
+
+var URL_TO_TILE_PY = "http://yourserver/tile.py";
+
+var map;
+
+function load() {
+	// Set up the initial map state.
+	if(document.implementation.hasFeature(
+	  "http://www.w3.org/TR/SVG11/feature#SVG","1.1")){ 
+		_mSvgEnabled = true;
+		_mSvgForced  = true;
+	}
+	map = new GMap2(document.getElementById("map"));
+	map.addControl(new GLargeMapControl());
+	map.addControl(new GMapTypeControl());
+	var omc = new GOverviewMapControl();
+	map.addControl(omc);
+	map.setCenter(new GLatLng(-42.423457,-60.292969),4);
+
+    var tilelayer = new GTileLayer(new GCopyrightCollection(), 0, 17);
+    tilelayer.getTileUrl = function(tile, zoom) {
+        var url = URL_TO_TILE_PY + "?x="+tile.x+"&y="+tile.y+"&z="+zoom;
+        //alert ("Called getTileUrl ("+tile.x+"/"+tile.y+", "+zoom+")\nReturning: " + url);
+        return url;
+    };
+    tilelayer.getOpacity = function() {return 0.6;}
+    map.addOverlay(new GTileLayerOverlay(tilelayer));
+
+}
+
+  </script>
+</head>
+<body onload="load()" onunload="GUnload()">
+<h2>The Map</h2>
+<div id="map" style="width: 800px; height: 500px"></div>
+</body>
+</html>
+

Added: trunk/thuban/Extensions/gMapTiles/sample/tile.py
===================================================================
--- trunk/thuban/Extensions/gMapTiles/sample/tile.py	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/sample/tile.py	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+import cgi
+import cgitb
+cgitb.enable()
+import cStringIO
+
+PATH_TO_TILES = "/path/to/tiles"
+
+size = 256 #image width and height
+
+def getTile(x, y, z):
+    z = int(z)
+    y = int(y)
+    x = int(x)
+    
+    try:
+        f = open('%s/%d/%d.%d.png' % (PATH_TO_TILES, z, x, y))
+    except IOError:
+        print "Content-type: text/plain\n\nNothing"
+        return
+
+    print "Content-type: image/png\n"
+    print f.read()
+
+if __name__ == "__main__":
+    form = cgi.FieldStorage()
+    if "x" in form and "y" in form and "z" in form:
+        getTile(form["x"].value, form["y"].value, form["z"].value)
+    else:
+        print "Content-type: text/html\n"
+        print """<html><body>Missing input arguments</body></html>"""
+


Property changes on: trunk/thuban/Extensions/gMapTiles/sample/tile.py
___________________________________________________________________
Name: svn:executable
   + *

Added: trunk/thuban/Extensions/gMapTiles/test/test_gMapTileExport.py
===================================================================
--- trunk/thuban/Extensions/gMapTiles/test/test_gMapTileExport.py	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Extensions/gMapTiles/test/test_gMapTileExport.py	2008-07-30 05:52:31 UTC (rev 2861)
@@ -0,0 +1,59 @@
+import unittest
+import os, sys
+from Thuban.Model.layer import Layer
+from Thuban.Model.map import Map
+from Thuban.Model.session import Session
+from Thuban.UI.context import Context
+from Thuban.Lib.connector import ConnectorError
+
+mapscriptAvailable=True
+try:
+    import mapscript
+    from Extensions.gMapTiles.exporter import export
+except ImportError:
+    mapscriptAvailable=False
+
+def rm_rf(d):
+    for path in (os.path.join(d,f) for f in os.listdir(d)):
+        if os.path.isdir(path):
+            rm_rf(path)
+        else:
+            os.unlink(path)
+    os.rmdir(d)
+
+class DummyMainWindow(object):
+    def __init__(self, canvas):
+        self.canvas = canvas
+
+class DummyCanvas(object):
+    def __init__(self, map):
+        self.map = map
+    def Map(self):
+        return self.map
+    def VisibleExtent (self):
+        return self.map.BoundingBox()
+
+class TestGMapTileExport(unittest.TestCase):
+    def setUp(self):
+        self.session = Session("A Session")
+        self.map = Map("A Map")
+        self.session.AddMap(self.map)
+        shapefile = "../../../Data/iceland/political.shp"
+        self.store = self.session.OpenShapefile(shapefile)
+        layer = Layer("A Layer", self.store)
+        self.map.AddLayer(layer)
+
+    def testExport(self):
+        tileDir = 'temp'
+        if not mapscriptAvailable:
+            #Skip test...
+            return
+        mainwindow = DummyMainWindow(DummyCanvas(self.map))
+        context = Context(None, self.session, mainwindow)
+        export (context, 7, 7, tileDir)
+        self.session.Destroy()
+        rm_rf(tileDir)
+
+if __name__ == "__main__":
+    unittest.main()
+

Modified: trunk/thuban/Thuban/thuban_cfg.py
===================================================================
--- trunk/thuban/Thuban/thuban_cfg.py	2008-07-30 03:18:02 UTC (rev 2860)
+++ trunk/thuban/Thuban/thuban_cfg.py	2008-07-30 05:52:31 UTC (rev 2861)
@@ -35,6 +35,11 @@
     print x
 
 try:
+    import Extensions.gMapTiles
+except Exception, x:
+    print x
+
+try:
     import Extensions.mouseposition
 except Exception, x:
     print x



More information about the Thuban-commits mailing list