Index: test/test_baserenderer.py =================================================================== --- test/test_baserenderer.py (revisión: 2850) +++ test/test_baserenderer.py (copia de trabajo) @@ -32,8 +32,7 @@ import Thuban.Model.resource -from Thuban.UI.baserenderer import BaseRenderer, \ - add_renderer_extension, init_renderer_extensions +from Thuban.UI.baserenderer import BaseRenderer, add_renderer_extension, init_renderer_extensions, PointClipper if Thuban.Model.resource.has_gdal_support(): from gdalwarp import ProjectRasterFile @@ -592,6 +591,108 @@ self.assertEquals(calls, [(renderer, layer)]) +class TestPointClipper (unittest.TestCase): + class Point: + def __init__(self, x, y): + self.x = x + self.y = y + def __repr__(self): + return "(%d,%d)" % (self.x,self.y) + def __eq__(self, p): + return self.x == p.x and self.y == p.y + def test_intersection (self): + P = self.Point + clipper = PointClipper (P, 40) + cases = [(P(10, 10), P(10, 100), P(10, 40)), + (P(10, 10), P(100, 10), P(40, 10)), + (P(10, 10), P(100, 100), P(40, 40)), + (P(10, 10), P(10, -100), P(10, -40)), + (P(10, 10), P(-100, 10), P(-40, 10)), + (P(30, 30), P(40, 40), P(40, 40)), + (P(30, 30), P(40, 30), P(40, 30)), + ] + for c in cases: + self.assertEquals (clipper.intersection(c[0], c[1]), c[2]) + self.assertRaises(ValueError, clipper.intersection,P(50, 50),P(60, 50)) + + def test_intersects (self): + P = self.Point + clipper = PointClipper (P, 40) + cases = [(40, 80, -80, -40, [P(0, 40), P(-40, 0)]), + (-50, 80, -80, 60, []), + (-50, 110, 50, 10, [P(20, 40), P(40, 20)]), + (-80, 50, 60, -20, [P(-40, 30), P(40, -10)]), + (-80, -50, 60, 20, [P(-40, -30), P(40, 10)]), + (-80, 80, -80, -80, []), + (3, 80, 3, -80, [P(3, 40), P(3, -40)]), + (-80, 3, 80, 19, [P(-40, 7), P(40, 15)]), + (-80, 28, 80, 44, [P(-40, 32), P(40,40)]), + (-50, 0, 50, -100, [P(-40, -10), P(-10, -40)]), + (38, -63, 38, -60, []), + (40, 30, 30, 40, [P(40, 30), P(30, 40)]), + (40, 40, 41, -40, []), + (40, 40, 40, 40, []), + (-40, -40, -40, -44, []), + ] + for c in cases: + result = clipper.intersects(c[0], c[1], c[2], c[3]) + self.assertEquals (result, c[4]) + + def test_clipping (self): + P = self.Point + clipper = PointClipper (P, 40) + cases = [[P(10,10), P(20,20)], + [P(10,10), P(100,10), P(100,20), P(10,20)], + [P(100,10), P(100,20), P(10,20), P(10,10)], + [P(100,20), P(10,20), P(10,10), P(100,10)], + [P(10,20), P(10,10), P(100,10), P(100,20)], + [P(70,20), P(10,20), P(10,10), P(70,20)], + [P(40,80), P(-80,-40), P(30,-50)], + [P(-100,-100), P(-100,100), P(100,100), P(100,-100)], + [P(-100,-100),P(-100,100),P(100,100),P(100,-100),P(-100,-100)], + [P(40,80), P(-80,-40), P(-80,80), P(40,80)], + [P(80,120), P(-120,-80), P(80,-80), P(80,120)], + [P(100, 100), P(100, -100)], + [P(50,-100), P(50,100), P(-50,0), P(50, -100)], + [P(-10,50), P(-50,50), P(-50,-50), P(-10,-50), P(-10,50)], + [P(-10,-50), P(-50,-50), P(-50,50), P(-10,50), P(-10,-50)], + [P(10,-50), P(50,-50), P(50,50), P(10,50), P(10,-50)], + [P(10,-50), P(50,-50), P(50,50), P(10,50), P(10,-50)], + [P(10,50), P(50,50), P(50,-50), P(10,-50), P(10,50)], + ] + correct = [[P(10,10), P(20,20)], + [P(10,10), P(40,10), P(40,20), P(10,20)], + [P(40,20), P(10,20), P(10,10)], + [P(40,20), P(10,20), P(10,10), P(40,10)], + [P(10,20), P(10,10), P(40,10)], + [P(40,20), P(10,20), P(10,10), P(40,15), P(40, 20)], + [P(0,40), P(-40,0)], + [], + [P(-40,-40), P(-40,40), P(40,40), P(40,-40), P(-40,-40)], + [P(0,40), P(-40,0), P(-40,40), P(0,40)], + [P(0,40), P(-40,0), P(-40,-40), P(40,-40), P(40,40),P(0,40)], + [], + [P(-10,40), P(-40,10), P(-40,-10), P(-10,-40), + P(40,-40), P(40,40), P(-10,40)], + [P(-10,-40), P(-10,40), P(-40,40), P(-40,-40), P(-10,-40)], + [P(-10,40), P(-10,-40), P(-40,-40), P(-40,40), P(-10,40)], + [P(10,40), P(10,-40), P(40,-40), P(40,40), P(10,40)], + [P(10,40), P(10,-40), P(40,-40), P(40,40), P(10,40)], + [P(10,-40), P(10,40), P(40,40), P(40,-40), P(10,-40)], + ] + for i in range(len(cases)): + self.assertEquals (clipper.clip (cases[i]), correct[i]) + + def test_closed_clipping(self): + P = self.Point + clipper = PointClipper(P, 40) + cases = [[P(50, -10), P(52, 3), P(54, 10), P(55, 8), P(52, 2)], + [P(50, -10), P(52, 3), P(54, 10), P(55, 8), P(50, -10)]] + results = [[], []] + for i in range(len(cases)): + self.assertEquals (clipper.clip(cases[i], closed=True), results[i]) + + if __name__ == "__main__": support.run_tests() Index: Thuban/UI/renderer.py =================================================================== --- Thuban/UI/renderer.py (revisión: 2850) +++ Thuban/UI/renderer.py (copia de trabajo) @@ -37,7 +37,7 @@ from Thuban.Model.color import Transparent import Thuban.Model.resource -from baserenderer import BaseRenderer +from baserenderer import BaseRenderer, PointClipper from math import floor @@ -66,7 +66,7 @@ TRANSPARENT_PEN = wx.TRANSPARENT_PEN TRANSPARENT_BRUSH = wx.TRANSPARENT_BRUSH - + clipper = PointClipper(wx.Point, 32000) def make_point(self, x, y): return wx.Point(int(round(x)), int(round(y))) @@ -95,6 +95,8 @@ wxproj.draw_polygon_shape and its corresponding parameter created with wxproj.draw_polygon_init. """ + # Skip optimized renderers as they don't support clipping yet + return BaseRenderer.low_level_renderer(self, layer) if (layer.ShapeStore().RawShapeFormat() == RAW_SHAPEFILE and layer.ShapeType() in (SHAPETYPE_ARC, SHAPETYPE_POLYGON)): offx, offy = self.offset Index: Thuban/UI/baserenderer.py =================================================================== --- Thuban/UI/baserenderer.py (revisión: 2850) +++ Thuban/UI/baserenderer.py (copia de trabajo) @@ -53,6 +53,8 @@ _renderer_extensions = [] +import math + def add_renderer_extension(layer_class, function): """Add a renderer extension for the layer class layer_class @@ -82,6 +84,303 @@ str += "+" + p + " " return str +class PointClipper: + def __init__ (self, pointClass, limit=32000): + """ This class filters a list of points to fit within a square area + centered around (0, 0). + 'pointClass' is the class that shall be instantiated to create new + points if they have to be added to the list. + 'limit' is half the side of the clipping square: All points will + be clipped within (-limit,+limit) in x and y. """ + self.lim = limit + self.Point = pointClass + + def intersection (self, a, b): + """ 'a' must be inside the clipping square, 'b' must be outside. + This function will return the point on the clipping square + that's also on the segment ab """ + if a.x == b.x: + if a.y < b.y: + return self.Point(a.x, self.lim) + else: + return self.Point(a.x, -self.lim) + m = (b.y-a.y)/float(b.x-a.x) + if a.x < b.x: + iy = (self.lim - a.x) * m + a.y + if -self.lim <= iy and iy <= self.lim: + return self.Point(self.lim, int(iy)) + if a.y < b.y: + ix = (self.lim - a.y) / m + a.x + if -self.lim <= ix and ix <= self.lim: + return self.Point(int(ix), self.lim) + if a.x > b.x: + iy = (-self.lim - a.x) * m + a.y + if -self.lim <= iy and iy <= self.lim: + return self.Point(-self.lim, int(iy)) + if a.y > b.y: + ix = (-self.lim - a.y) / m + a.x + if -self.lim <= ix and ix <= self.lim: + return self.Point(int(ix), -self.lim) + raise ValueError, (a, b) # It seems that 'a' is not inside the + # clipping square, or 'b' is not outside. + + def intersects (self, ax, ay, bx, by): + """ Both 'a' and 'b' must be outside the clipping square. The segment + ab intersects the limit square twice or never. This function + returns a list with both intersections if they exist, or an empty + list if ab never intersects the clipping square.""" + # First get rid of cases that break slope calculations + if ax == bx: + if -self.lim <= ax and ax <= self.lim: + if ay >= self.lim and by <= -self.lim: + return [self.Point(ax, self.lim), self.Point(ax, -self.lim)] + elif ay <= -self.lim and by >= self.lim: + return [self.Point(ax, -self.lim), self.Point(ax, self.lim)] + else: + return [] + else: + return [] + # 64 cases left: ax, ay, bx and by can all be within +-self.lim, + # or < -self.lim, or > self.lim, but x and y can't be within +-self.lim + # at the same time. + + # So, next we take care of 32 easy cases. Only 32 more to go + elif ((ay > self.lim and by > self.lim) or + (ay < -self.lim and by < -self.lim) or + (ax > self.lim and bx > self.lim) or + (ax < -self.lim and bx < -self.lim)): + return [] + else: + # Next take care of 22 cases where ay isn't within +-self.lim + m = (by - ay) / float(bx - ax) + f = 1 + if ay < -self.lim: + ay = -ay + by = -by + m = -m + f = -1 + if ay > self.lim: + fx = 1 + if ax > self.lim: + ax = -ax + bx = -bx + fx = -1 + m = -m + if ax < -self.lim: + iy = (-self.lim - ax) * m + ay + if -self.lim <= iy and iy <= self.lim: + ai = self.Point (-self.lim * fx, iy * f) + iy2 = (self.lim - ax) * m + ay + if -self.lim <= iy2 and iy2 <= self.lim: + return [ai, self.Point (self.lim * fx, iy2 * f)] + else: + ix2 = (-self.lim - ay) / m + ax + return [ai, self.Point(ix2 * fx, -self.lim * f)] + else: + ix = (self.lim - ay) / m + ax + if -self.lim <= ix and ix <= self.lim: + ai = self.Point (ix * fx, self.lim * f) + ix2 = (-self.lim - ay) / m + ax + if -self.lim <= ix2 and ix2 <= self.lim: + return [ai, self.Point(ix2 * fx, -self.lim * f)] + else: + iy2 = (self.lim - ax) * m + ay + return [ai, self.Point(self.lim * fx, iy2 * f)] + else: + return [] + else: + ix = (self.lim - ay) / m + ax + if -self.lim <= ix and ix <= self.lim: + ai = self.Point (ix, self.lim * f) + ix2 = (-self.lim - ay) / m + ax + if -self.lim <= ix2 and ix2 <= self.lim: + return [ai, self.Point(ix2, -self.lim * f)] + elif bx > self.lim: + iy2 = (self.lim - ax) * m + ay + return [ai, self.Point(self.lim, iy2 * f)] + else: + iy2 = (-self.lim - ax) * m + ay + return [ai, self.Point(-self.lim, iy2 * f)] + else: + return [] + else: + # Last 10 cases + fx = 1 + if ax >= self.lim: + ax = -ax + bx = -bx + m = -m + fx = -1 + iy = (-self.lim - ax) * m + ay + if -self.lim < iy and iy < self.lim: + ai = self.Point (-self.lim * fx, iy) + fy = 1 + if by >= self.lim: + ay = -ay + by = -by + m = -m + fy = -1 + if bx < self.lim: + ix2 = (-self.lim - ay) / m + ax + if -self.lim <= ix2 and ix2 <= self.lim: + return [ai, self.Point (ix2 * fx, -self.lim * fy)] + else: + raise ValueError, (ax, ay, bx, by) + # Add this case to the tests and make + # it pass (intersects should never raise)! + else: + iy2 = (self.lim - ax) * m + ay + if -self.lim <= iy2 and iy2 <= self.lim: + return [ai, self.Point (self.lim * fx, iy2 * fy)] + else: + ix2 = (-self.lim - ay) / m + ax + if -self.lim <= ix2 and ix2 <= self.lim: + return [ai, self.Point(ix2*fx, -self.lim * fy)] + else: + raise ValueError, (ax, ay, bx, by) + # Add this case to the tests and make + # it pass (intersects should never raise)! + else: + return [] + + def attach (self, points, newPoint, angle): + """ Append newPoint to points, possibly after other new points + if newPoint isn't on the same side of the clipping square as + points[-1] """ + def side(p): + if p.y == -self.lim: + return 0 + elif p.x == -self.lim: + return 1 + elif p.y == self.lim: + return 2 + else: + return 3 + corners = [self.Point (-self.lim, -self.lim), + self.Point (-self.lim, self.lim), + self.Point (self.lim, self.lim), + self.Point (self.lim, -self.lim)] + if len(points) > 0: + i = side(points[-1]) + if angle < 0: + while i != side(newPoint): + points.append (corners[i]) + i = (i + 1) % 4 + else: + while i != side(newPoint): + i = (i - 1) % 4 + points.append (corners[i]) + points.append (newPoint) + + + def clip (self, points, closed=False): + result = [] + lastPoint = None + angle = 0 + lastAngle = 0 + initialAngle = 0 + for i in range(len(points)): + if i > 0: + newAngle = math.atan2(points[i].y, points[i].x) + if abs(newAngle-lastAngle) > math.pi: + if newAngle > lastAngle + math.pi: + lastAngle += 2*math.pi + else: + lastAngle -= 2*math.pi + angle += newAngle - lastAngle + lastAngle = newAngle + else: + lastAngle = math.atan2(points[i].y, points[i].x) + if (-self.lim <= points[i].x and points[i].x <= self.lim and + -self.lim < points[i].y and points[i].y < self.lim): + if not lastPoint: + # First point inside, simply append + result.append (points[i]) + elif lastPointInside: + # Both points inside, simply append + result.append (points[i]) + else: + # last Point not inside, but this one is: Add a segment + # from the border to the point + border = self.intersection(points[i], lastPoint) + self.attach (result, border, angle) + result.append (points[i]) + lastPointInside = True + else: + if not lastPoint: + # First point outside, do nothing + pass + elif lastPointInside: + # last point inside, this one outside: Add a segment from + # point to the border. + border = self.intersection(lastPoint, points[i]) + result.append (border) + angle = 0 + else: + # Both points outside: check if they cross any border, and + # if so add a segment + segment = self.intersects (lastPoint.x, lastPoint.y, + points[i].x, points[i].y) + if len(segment) == 2: + self.attach (result, segment[0], angle) + result.append (segment[1]) + angle = 0 + lastPointInside = False + + lastPoint = points[i] + if len(result) == 0: + initialAngle = angle + # Make the output a closed polygon if the input is a closed polygon, or + # if we were requested a closed polygon + if len(result) > 0: + if (points[0] == points[-1] and result[0] != result[-1]) or closed: + if lastPointInside: + result.append (result[0]) + else: + angle += initialAngle + self.attach (result, result[0], angle) + # Finally, check if we're lost in the middle of a huge area + if len(result) == 0 and ((points[0] == points[-1]) or closed): + ends = [] + crosses = 0 + for i in range(len(points)): + a = points[i] + if i == len(points) -1: + if points[i] == points[0]: + continue + else: + b = points[0] + else: + b = points[i + 1] + if (a.y > 0 and b.y > 0) or (a.y < 0 and b.y < 0): + pass + elif a.y == 0: + if not a.x in ends: + ends.append (a.x) + crosses += 1 + elif b.y == 0: + if not b.x in ends: + ends.append (b.x) + crosses += 1 + else: # (a.y > 0 and b.y < 0) or (a.y < 0 and b.y > 0) + if a.x >= 0 and b.x >= 0: + crosses += 1 + elif a.x < 0 and b.x < 0: + pass + else: + xi = (a.y * (b.x - a.x) - a.x) / float (b.y - a.y) + if xi > 0: + crosses += 1 + if crosses % 2 == 1: + result = [self.Point (-self.lim, -self.lim), + self.Point (-self.lim, self.lim), + self.Point (self.lim, self.lim), + self.Point (self.lim, -self.lim), + self.Point (-self.lim, -self.lim)] + return result + + + # # Base Renderer # @@ -379,6 +678,8 @@ For a description of the algorithm look in wxproj.cpp. """ points = self.projected_points(layer, points) + if hasattr(self, 'clipper'): + points = [self.clipper.clip(l, closed=True) for l in points] if brush is not self.TRANSPARENT_BRUSH: polygon = [] @@ -410,6 +711,8 @@ self.projected_points. """ points = self.projected_points(layer, points) + if hasattr(self, 'clipper'): + points = [self.clipper.clip(l) for l in points] self.dc.SetBrush(brush) self.dc.SetPen(pen) for part in points: @@ -428,6 +731,8 @@ points = self.projected_points(layer, points) if not points: return + if hasattr(self, 'clipper'): + points = [self.clipper.clip(l) for l in points] radius = int(round(self.resolution * size)) self.dc.SetBrush(brush) Index: Thuban/UI/viewport.py =================================================================== --- Thuban/UI/viewport.py (revisión: 2850) +++ Thuban/UI/viewport.py (copia de trabajo) @@ -385,7 +385,7 @@ # into 16bit signed integers. max_len = max(pwidth, pheight) if max_len: - max_scale = 32767.0 / max_len + max_scale = 500000 else: # FIXME: What to do in this case? The bbox is effectively # empty so any scale should work.