|
@@ -1,12 +1,13 @@
|
|
|
"""!
|
|
|
@package psmap.utils
|
|
|
|
|
|
-@brief utilities for wxpsmap
|
|
|
+@brief utilities for wxpsmap (classes, functions)
|
|
|
|
|
|
Classes:
|
|
|
- utils::Rect2D
|
|
|
- utils::Rect2DPP
|
|
|
- utils::Rect2DPS
|
|
|
+ - utils::UnitConversion
|
|
|
|
|
|
(C) 2012 by Anna Kratochvilova, and the GRASS Development Team
|
|
|
This program is free software under the GNU General Public License
|
|
@@ -16,6 +17,16 @@ This program is free software under the GNU General Public License
|
|
|
"""
|
|
|
|
|
|
import wx
|
|
|
+from math import ceil, floor, sin, cos, pi
|
|
|
+
|
|
|
+try:
|
|
|
+ import Image as PILImage
|
|
|
+ havePILImage = True
|
|
|
+except ImportError:
|
|
|
+ havePILImage = False
|
|
|
+
|
|
|
+import grass.script as grass
|
|
|
+from core.gcmd import RunCommand
|
|
|
|
|
|
class Rect2D(wx.Rect2D):
|
|
|
"""!Class representing rectangle with floating point values.
|
|
@@ -68,3 +79,350 @@ class Rect2DPS(Rect2D):
|
|
|
"""
|
|
|
def __init__(self, pos = wx.Point2D(), size = (0, 0)):
|
|
|
Rect2D.__init__(self, x = pos[0], y = pos[1], width = size[0], height = size[1])
|
|
|
+
|
|
|
+class UnitConversion:
|
|
|
+ """! Class for converting units"""
|
|
|
+ def __init__(self, parent = None):
|
|
|
+ self.parent = parent
|
|
|
+ if self.parent:
|
|
|
+ ppi = wx.ClientDC(self.parent).GetPPI()
|
|
|
+ else:
|
|
|
+ ppi = (72, 72)
|
|
|
+ self._unitsPage = { 'inch' : {'val': 1.0, 'tr' : _("inch")},
|
|
|
+ 'point' : {'val': 72.0, 'tr' : _("point")},
|
|
|
+ 'centimeter' : {'val': 2.54, 'tr' : _("centimeter")},
|
|
|
+ 'millimeter' : {'val': 25.4, 'tr' : _("millimeter")}}
|
|
|
+ self._unitsMap = { 'meters' : {'val': 0.0254, 'tr' : _("meters")},
|
|
|
+ 'kilometers' : {'val': 2.54e-5, 'tr' : _("kilometers")},
|
|
|
+ 'feet' : {'val': 1./12, 'tr' : _("feet")},
|
|
|
+ 'miles' : {'val': 1./63360, 'tr' : _("miles")},
|
|
|
+ 'nautical miles': {'val': 1/72913.386, 'tr' : _("nautical miles")}}
|
|
|
+
|
|
|
+ self._units = { 'pixel' : {'val': ppi[0], 'tr' : _("pixel")},
|
|
|
+ 'meter' : {'val': 0.0254, 'tr' : _("meter")},
|
|
|
+ 'nautmiles' : {'val': 1/72913.386, 'tr' :_("nautical miles")},
|
|
|
+ 'degrees' : {'val': 0.0254 , 'tr' : _("degree")} #like 1 meter, incorrect
|
|
|
+ }
|
|
|
+ self._units.update(self._unitsPage)
|
|
|
+ self._units.update(self._unitsMap)
|
|
|
+
|
|
|
+ def getPageUnitsNames(self):
|
|
|
+ return sorted(self._unitsPage[unit]['tr'] for unit in self._unitsPage.keys())
|
|
|
+
|
|
|
+ def getMapUnitsNames(self):
|
|
|
+ return sorted(self._unitsMap[unit]['tr'] for unit in self._unitsMap.keys())
|
|
|
+
|
|
|
+ def getAllUnits(self):
|
|
|
+ return sorted(self._units.keys())
|
|
|
+
|
|
|
+ def findUnit(self, name):
|
|
|
+ """!Returns unit by its tr. string"""
|
|
|
+ for unit in self._units.keys():
|
|
|
+ if self._units[unit]['tr'] == name:
|
|
|
+ return unit
|
|
|
+ return None
|
|
|
+
|
|
|
+ def findName(self, unit):
|
|
|
+ """!Returns tr. string of a unit"""
|
|
|
+ try:
|
|
|
+ return self._units[unit]['tr']
|
|
|
+ except KeyError:
|
|
|
+ return None
|
|
|
+
|
|
|
+ def convert(self, value, fromUnit = None, toUnit = None):
|
|
|
+ return float(value)/self._units[fromUnit]['val']*self._units[toUnit]['val']
|
|
|
+
|
|
|
+def convertRGB(rgb):
|
|
|
+ """!Converts wx.Colour(r,g,b,a) to string 'r:g:b' or named color,
|
|
|
+ or named color/r:g:b string to wx.Colour, depending on input"""
|
|
|
+ # transform a wx.Colour tuple into an r:g:b string
|
|
|
+ if type(rgb) == wx.Colour:
|
|
|
+ for name, color in grass.named_colors.items():
|
|
|
+ if rgb.Red() == int(color[0] * 255) and\
|
|
|
+ rgb.Green() == int(color[1] * 255) and\
|
|
|
+ rgb.Blue() == int(color[2] * 255):
|
|
|
+ return name
|
|
|
+ return str(rgb.Red()) + ':' + str(rgb.Green()) + ':' + str(rgb.Blue())
|
|
|
+ # transform a GRASS named color or an r:g:b string into a wx.Colour tuple
|
|
|
+ else:
|
|
|
+ color = (grass.parse_color(rgb)[0]*255,
|
|
|
+ grass.parse_color(rgb)[1]*255,
|
|
|
+ grass.parse_color(rgb)[2]*255)
|
|
|
+ color = wx.Color(*color)
|
|
|
+ if color.IsOk():
|
|
|
+ return color
|
|
|
+ else:
|
|
|
+ return None
|
|
|
+
|
|
|
+def PaperMapCoordinates(map, x, y, paperToMap = True):
|
|
|
+ """!Converts paper (inch) coordinates -> map coordinates"""
|
|
|
+ unitConv = UnitConversion()
|
|
|
+ currRegionDict = grass.region()
|
|
|
+ cornerEasting, cornerNorthing = currRegionDict['w'], currRegionDict['n']
|
|
|
+ xMap = map['rect'][0]
|
|
|
+ yMap = map['rect'][1]
|
|
|
+ widthMap = map['rect'][2] * 0.0254 # to meter
|
|
|
+ heightMap = map['rect'][3] * 0.0254
|
|
|
+ xScale = widthMap / abs(currRegionDict['w'] - currRegionDict['e'])
|
|
|
+ yScale = heightMap / abs(currRegionDict['n'] - currRegionDict['s'])
|
|
|
+ currScale = (xScale + yScale) / 2
|
|
|
+
|
|
|
+ if not paperToMap:
|
|
|
+ textEasting, textNorthing = x, y
|
|
|
+ eastingDiff = textEasting - cornerEasting
|
|
|
+ if currRegionDict['w'] > currRegionDict['e']:
|
|
|
+ eastingDiff = - eastingDiff
|
|
|
+ else:
|
|
|
+ eastingDiff = eastingDiff
|
|
|
+
|
|
|
+ northingDiff = textNorthing - cornerNorthing
|
|
|
+ if currRegionDict['n'] > currRegionDict['s']:
|
|
|
+ northingDiff = - northingDiff
|
|
|
+ else:
|
|
|
+ northingDiff = northingDiff
|
|
|
+
|
|
|
+ xPaper = xMap + unitConv.convert(value = eastingDiff, fromUnit = 'meter', toUnit = 'inch') * currScale
|
|
|
+ yPaper = yMap + unitConv.convert(value = northingDiff, fromUnit = 'meter', toUnit = 'inch') * currScale
|
|
|
+ return xPaper, yPaper
|
|
|
+ else:
|
|
|
+ if currRegionDict['w'] < currRegionDict['e']:
|
|
|
+ eastingDiff = (x - xMap)
|
|
|
+ else:
|
|
|
+ eastingDiff = (xMap - x)
|
|
|
+ if currRegionDict['n'] < currRegionDict['s']:
|
|
|
+ northingDiff = (y - yMap)
|
|
|
+ else:
|
|
|
+ northingDiff = (yMap - y)
|
|
|
+
|
|
|
+ textEasting = cornerEasting + unitConv.convert(value = eastingDiff, fromUnit = 'inch', toUnit = 'meter') / currScale
|
|
|
+ textNorthing = cornerNorthing + unitConv.convert(value = northingDiff, fromUnit = 'inch', toUnit = 'meter') / currScale
|
|
|
+ return int(textEasting), int(textNorthing)
|
|
|
+
|
|
|
+def AutoAdjust(self, scaleType, rect, map = None, mapType = None, region = None):
|
|
|
+ """!Computes map scale, center and map frame rectangle to fit region (scale is not fixed)"""
|
|
|
+ currRegionDict = {}
|
|
|
+ if scaleType == 0 and map:# automatic, region from raster or vector
|
|
|
+ res = ''
|
|
|
+ if mapType == 'raster':
|
|
|
+ try:
|
|
|
+ res = grass.read_command("g.region", flags = 'gu', rast = map)
|
|
|
+ except grass.ScriptError:
|
|
|
+ pass
|
|
|
+ elif mapType == 'vector':
|
|
|
+ res = grass.read_command("g.region", flags = 'gu', vect = map)
|
|
|
+ currRegionDict = grass.parse_key_val(res, val_type = float)
|
|
|
+ elif scaleType == 1 and region: # saved region
|
|
|
+ res = grass.read_command("g.region", flags = 'gu', region = region)
|
|
|
+ currRegionDict = grass.parse_key_val(res, val_type = float)
|
|
|
+ elif scaleType == 2: # current region
|
|
|
+ env = grass.gisenv()
|
|
|
+ windFilePath = os.path.join(env['GISDBASE'], env['LOCATION_NAME'], env['MAPSET'], 'WIND')
|
|
|
+ try:
|
|
|
+ windFile = open(windFilePath, 'r').read()
|
|
|
+ except IOError:
|
|
|
+ currRegionDict = grass.region()
|
|
|
+ regionDict = grass.parse_key_val(windFile, sep = ':', val_type = float)
|
|
|
+ region = grass.read_command("g.region", flags = 'gu', n = regionDict['north'], s = regionDict['south'],
|
|
|
+ e = regionDict['east'], w = regionDict['west'])
|
|
|
+ currRegionDict = grass.parse_key_val(region, val_type = float)
|
|
|
+
|
|
|
+ else:
|
|
|
+ return None, None, None
|
|
|
+
|
|
|
+ if not currRegionDict:
|
|
|
+ return None, None, None
|
|
|
+ rX = rect.x
|
|
|
+ rY = rect.y
|
|
|
+ rW = rect.width
|
|
|
+ rH = rect.height
|
|
|
+ if not hasattr(self, 'unitConv'):
|
|
|
+ self.unitConv = UnitConversion(self)
|
|
|
+ toM = 1
|
|
|
+ if projInfo()['proj'] != 'xy':
|
|
|
+ toM = float(projInfo()['meters'])
|
|
|
+
|
|
|
+ mW = self.unitConv.convert(value = (currRegionDict['e'] - currRegionDict['w']) * toM, fromUnit = 'meter', toUnit = 'inch')
|
|
|
+ mH = self.unitConv.convert(value = (currRegionDict['n'] - currRegionDict['s']) * toM, fromUnit = 'meter', toUnit = 'inch')
|
|
|
+ scale = min(rW/mW, rH/mH)
|
|
|
+
|
|
|
+ if rW/rH > mW/mH:
|
|
|
+ x = rX - (rH*(mW/mH) - rW)/2
|
|
|
+ y = rY
|
|
|
+ rWNew = rH*(mW/mH)
|
|
|
+ rHNew = rH
|
|
|
+ else:
|
|
|
+ x = rX
|
|
|
+ y = rY - (rW*(mH/mW) - rH)/2
|
|
|
+ rHNew = rW*(mH/mW)
|
|
|
+ rWNew = rW
|
|
|
+
|
|
|
+ # center
|
|
|
+ cE = (currRegionDict['w'] + currRegionDict['e'])/2
|
|
|
+ cN = (currRegionDict['n'] + currRegionDict['s'])/2
|
|
|
+ return scale, (cE, cN), Rect2D(x, y, rWNew, rHNew) #inch
|
|
|
+
|
|
|
+def SetResolution(dpi, width, height):
|
|
|
+ """!If resolution is too high, lower it
|
|
|
+
|
|
|
+ @param dpi max DPI
|
|
|
+ @param width map frame width
|
|
|
+ @param height map frame height
|
|
|
+ """
|
|
|
+ region = grass.region()
|
|
|
+ if region['cols'] > width * dpi or region['rows'] > height * dpi:
|
|
|
+ rows = height * dpi
|
|
|
+ cols = width * dpi
|
|
|
+ RunCommand('g.region', rows = rows, cols = cols)
|
|
|
+
|
|
|
+def ComputeSetRegion(self, mapDict):
|
|
|
+ """!Computes and sets region from current scale, map center coordinates and map rectangle"""
|
|
|
+
|
|
|
+ if mapDict['scaleType'] == 3: # fixed scale
|
|
|
+ scale = mapDict['scale']
|
|
|
+
|
|
|
+ if not hasattr(self, 'unitConv'):
|
|
|
+ self.unitConv = UnitConversion(self)
|
|
|
+
|
|
|
+ fromM = 1
|
|
|
+ if projInfo()['proj'] != 'xy':
|
|
|
+ fromM = float(projInfo()['meters'])
|
|
|
+ rectHalfInch = (mapDict['rect'].width/2, mapDict['rect'].height/2)
|
|
|
+ rectHalfMeter = (self.unitConv.convert(value = rectHalfInch[0], fromUnit = 'inch', toUnit = 'meter')/ fromM /scale,
|
|
|
+ self.unitConv.convert(value = rectHalfInch[1], fromUnit = 'inch', toUnit = 'meter')/ fromM /scale)
|
|
|
+
|
|
|
+ centerE = mapDict['center'][0]
|
|
|
+ centerN = mapDict['center'][1]
|
|
|
+
|
|
|
+ raster = self.instruction.FindInstructionByType('raster')
|
|
|
+ if raster:
|
|
|
+ rasterId = raster.id
|
|
|
+ else:
|
|
|
+ rasterId = None
|
|
|
+
|
|
|
+ if rasterId:
|
|
|
+ RunCommand('g.region', n = ceil(centerN + rectHalfMeter[1]),
|
|
|
+ s = floor(centerN - rectHalfMeter[1]),
|
|
|
+ e = ceil(centerE + rectHalfMeter[0]),
|
|
|
+ w = floor(centerE - rectHalfMeter[0]),
|
|
|
+ rast = self.instruction[rasterId]['raster'])
|
|
|
+ else:
|
|
|
+ RunCommand('g.region', n = ceil(centerN + rectHalfMeter[1]),
|
|
|
+ s = floor(centerN - rectHalfMeter[1]),
|
|
|
+ e = ceil(centerE + rectHalfMeter[0]),
|
|
|
+ w = floor(centerE - rectHalfMeter[0]))
|
|
|
+
|
|
|
+def projInfo():
|
|
|
+ """!Return region projection and map units information,
|
|
|
+ taken from render.py"""
|
|
|
+
|
|
|
+ projinfo = dict()
|
|
|
+
|
|
|
+ ret = RunCommand('g.proj', read = True, flags = 'p')
|
|
|
+
|
|
|
+ if not ret:
|
|
|
+ return projinfo
|
|
|
+
|
|
|
+ for line in ret.splitlines():
|
|
|
+ if ':' in line:
|
|
|
+ key, val = line.split(':')
|
|
|
+ projinfo[key.strip()] = val.strip()
|
|
|
+ elif "XY location (unprojected)" in line:
|
|
|
+ projinfo['proj'] = 'xy'
|
|
|
+ projinfo['units'] = ''
|
|
|
+ break
|
|
|
+
|
|
|
+ return projinfo
|
|
|
+
|
|
|
+def GetMapBounds(filename, portrait = True):
|
|
|
+ """!Run ps.map -b to get information about map bounding box
|
|
|
+
|
|
|
+ @param filename psmap input file
|
|
|
+ @param portrait page orientation"""
|
|
|
+ orient = ''
|
|
|
+ if not portrait:
|
|
|
+ orient = 'r'
|
|
|
+ try:
|
|
|
+ bb = map(float, grass.read_command('ps.map',
|
|
|
+ flags = 'b' + orient,
|
|
|
+ quiet = True,
|
|
|
+ input = filename).strip().split('=')[1].split(','))
|
|
|
+ except (grass.ScriptError, IndexError):
|
|
|
+ GError(message = _("Unable to run `ps.map -b`"))
|
|
|
+ return None
|
|
|
+ return Rect2D(bb[0], bb[3], bb[2] - bb[0], bb[1] - bb[3])
|
|
|
+
|
|
|
+def getRasterType(map):
|
|
|
+ """!Returns type of raster map (CELL, FCELL, DCELL)"""
|
|
|
+ if map is None:
|
|
|
+ map = ''
|
|
|
+ file = grass.find_file(name = map, element = 'cell')
|
|
|
+ if file['file']:
|
|
|
+ rasterType = grass.raster_info(map)['datatype']
|
|
|
+ return rasterType
|
|
|
+ else:
|
|
|
+ return None
|
|
|
+
|
|
|
+def PilImageToWxImage(pilImage, copyAlpha = True):
|
|
|
+ """!Convert PIL image to wx.Image
|
|
|
+
|
|
|
+ Based on http://wiki.wxpython.org/WorkingWithImages
|
|
|
+ """
|
|
|
+ hasAlpha = pilImage.mode[-1] == 'A'
|
|
|
+ if copyAlpha and hasAlpha : # Make sure there is an alpha layer copy.
|
|
|
+ wxImage = wx.EmptyImage( *pilImage.size )
|
|
|
+ pilImageCopyRGBA = pilImage.copy()
|
|
|
+ pilImageCopyRGB = pilImageCopyRGBA.convert('RGB') # RGBA --> RGB
|
|
|
+ pilImageRgbData = pilImageCopyRGB.tostring()
|
|
|
+ wxImage.SetData(pilImageRgbData)
|
|
|
+ wxImage.SetAlphaData(pilImageCopyRGBA.tostring()[3::4]) # Create layer and insert alpha values.
|
|
|
+
|
|
|
+ else : # The resulting image will not have alpha.
|
|
|
+ wxImage = wx.EmptyImage(*pilImage.size)
|
|
|
+ pilImageCopy = pilImage.copy()
|
|
|
+ pilImageCopyRGB = pilImageCopy.convert('RGB') # Discard any alpha from the PIL image.
|
|
|
+ pilImageRgbData = pilImageCopyRGB.tostring()
|
|
|
+ wxImage.SetData(pilImageRgbData)
|
|
|
+
|
|
|
+ return wxImage
|
|
|
+
|
|
|
+def BBoxAfterRotation(w, h, angle):
|
|
|
+ """!Compute bounding box or rotated rectangle
|
|
|
+
|
|
|
+ @param w rectangle width
|
|
|
+ @param h rectangle height
|
|
|
+ @param angle angle (0, 360) in degrees
|
|
|
+ """
|
|
|
+ angleRad = angle / 180. * pi
|
|
|
+ ct = cos(angleRad)
|
|
|
+ st = sin(angleRad)
|
|
|
+
|
|
|
+ hct = h * ct
|
|
|
+ wct = w * ct
|
|
|
+ hst = h * st
|
|
|
+ wst = w * st
|
|
|
+ y = x = 0
|
|
|
+
|
|
|
+ if 0 < angle <= 90:
|
|
|
+ y_min = y
|
|
|
+ y_max = y + hct + wst
|
|
|
+ x_min = x - hst
|
|
|
+ x_max = x + wct
|
|
|
+ elif 90 < angle <= 180:
|
|
|
+ y_min = y + hct
|
|
|
+ y_max = y + wst
|
|
|
+ x_min = x - hst + wct
|
|
|
+ x_max = x
|
|
|
+ elif 180 < angle <= 270:
|
|
|
+ y_min = y + wst + hct
|
|
|
+ y_max = y
|
|
|
+ x_min = x + wct
|
|
|
+ x_max = x - hst
|
|
|
+ elif 270 < angle <= 360:
|
|
|
+ y_min = y + wst
|
|
|
+ y_max = y + hct
|
|
|
+ x_min = x
|
|
|
+ x_max = x + wct - hst
|
|
|
+
|
|
|
+ width = int(ceil(abs(x_max) + abs(x_min)))
|
|
|
+ height = int(ceil(abs(y_max) + abs(y_min)))
|
|
|
+ return width, height
|