#
# Copyright (c) 2002, 2003, 2004, 2005 Art Haas
#
# This file is part of PythonCAD.
#
# PythonCAD is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PythonCAD is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PythonCAD; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
# classes for line segments
#
# TODO: Look at using weak references


from __future__ import generators

import math

from PythonCAD.Generic import graphicobject
from PythonCAD.Generic import tolerance
from PythonCAD.Generic import style
from PythonCAD.Generic import linetype
from PythonCAD.Generic import color
from PythonCAD.Generic import point
from PythonCAD.Generic import util
from PythonCAD.Generic import quadtree

class Segment(graphicobject.GraphicObject):
    """A class representing a line segment.

A Segment has two attributes.

p1: A Point object representing the first end point
p2: A Point object representing the second end point

A Segment has the following methods:

getEndpoints(): Return the two endpoints of the Segment.
{get/set}P1: Get/Set the Segment first endpoint.
{get/set}P2: Get/Set the Segment second endpoint.
move(): Move the Segment.
length(): Get the Segment length.
getCoefficients(): Return the segment as ax + by + c = 0
getProjection(): Project a coordinate on to the Segment
mapCoords(): Test if a coordinate pair is within some distance to a Segment.
inRegion(): Test if the Segment is visible in some area.
clone(): Make an identical copy of a Segment.
    """
    messages = {
        'moved' : True,
        'endpoint_changed' : True
        }

    __defstyle = style.Style(u'Default Segment Style',
                            linetype.Linetype(u'Solid', None),
                            color.Color(0xffffff),
                            1.0)

    def __init__(self, p1, p2, st=None, lt=None, col=None, th=None, **kw):
        """Initialize a Segment object.

Segment(p1, p2[, st, lt, col, th])

p1: Segment first endpoint - may be a Point or a two-item tuple of floats.
p2: Segment second endpoint - may be a Point or a two-item tuple of floats.

The following arguments are optional:

st: A Style object
lt: A Linetype object that overrides the linetype in the Style.
col: A Color object that overrides the color in the Style.
th: A float that overrides the line thickness in the Style.
        """

        _p1 = p1
        if not isinstance(_p1, point.Point):
            _p1 = point.Point(p1)
        _p2 = p2
        if not isinstance(_p2, point.Point):
            _p2 = point.Point(p2)
        if _p1 is _p2:
            raise ValueError, "Segments cannot have identical endpoints."
        _st = st
        if _st is None:
            _st = Segment.__defstyle
        super(Segment, self).__init__(_st, lt, col, th, **kw)
        self.__p1 = _p1
        _p1.connect('moved', self._movePoint)
        _p1.storeUser(self)
        self.__p2 = _p2
        _p2.connect('moved', self._movePoint)
        _p2.storeUser(self)

    def __str__(self):
        return "Segment: %s to %s" % (self.__p1, self.__p2)

    def __eq__(self, obj):
        """Compare a Segment to another for equality.
        """
        if not isinstance(obj, Segment):
            return False
        if obj is self:
            return True
        _sp1 = self.__p1
        _sp2 = self.__p2
        _op1, _op2 = obj.getEndpoints()
        if ((_sp1 == _op1) and (_sp2 == _op2)):
            _val = True
        elif ((_sp1 == _op2) and (_sp2 == _op1)):
            _val = True
        else:
            _val = False
        return _val

    def __ne__(self, obj):
        """Compare a Segment to another for inequality.
        """
        if not isinstance(obj, Segment):
            return True
        if obj is self:
            return False
        _sp1 = self.__p1
        _sp2 = self.__p2
        _op1, _op2 = obj.getEndpoints()
        if ((_sp1 == _op1) and (_sp2 == _op2)):
            _val = False
        elif ((_sp1 == _op2) and (_sp2 == _op1)):
            _val = False
        else:
            _val = True
        return _val

    def finish(self):
        self.__p1.disconnect(self)
        self.__p1.freeUser(self)
        self.__p2.disconnect(self)
        self.__p2.freeUser(self)
        self.__p1 = self.__p2 = None
        super(Segment, self).finish()

    def setStyle(self, s):
        """Set the Style of the Segment

setStyle(s)

This method extends GraphicObject::setStyle().
        """
        _s = s
        if _s is None:
            _s = Segment.__defstyle
        super(Segment, self).setStyle(_s)

    def getValues(self):
        """Return values comprising the Segment.

getValues()

This method extends the GraphicObject::getValues() method.
        """
        _data = super(Segment, self).getValues()
        if self.getStyle() is Segment.__defstyle:
            _data.setValue('style', None)
        if False: # debug stuff ...
            _style = _data.get('style')
            if _style is not None:
                print "Segment style: " + str(_style)
            _lt = _data.get('linetype')
            if _lt is not None:
                print "Linetype : " + str(_lt)
            _col = _data.get('color')
            if _col is not None:
                print "Color: " + str(_col)
            _th = _data.get('thickness')
            if _th is not None:
                print "Thickness: %d" % _th
        _data.setValue('type', 'segment')
        _data.setValue('p1', self.__p1.getID())
        _data.setValue('p2', self.__p2.getID())
        return _data

    def getEndpoints(self):
        """Get the endpoints of the Segment.

getEndpoints()

This function returns a tuple containing the two Point objects
that are the endpoints of the segment.
        """
        return self.__p1, self.__p2

    def getP1(self):
        """Return the first endpoint Point of the Segment.

getP1()
        """
        return self.__p1

    def setP1(self, p):
        """Set the first endpoint Point of the Segment.

setP1(p)
        """
        if self.isLocked():
            raise RuntimeError, "Setting endpoint not allowed - object locked."
        if not isinstance(p, point.Point):
            raise TypeError, "Invalid P1 endpoint: " + str(p)
        if p is self.__p2:
            raise ValueError, "Segments cannot have identical endpoints."
        _pt = self.__p1
        if _pt is not p:
            _pt.disconnect(self)
            _pt.freeUser(self)
            self.__p1 = p
            self.sendMessage('endpoint_changed', _pt, p)
            p.storeUser(self)
            p.connect('moved', self._movePoint)
            if abs(_pt.x - p.x) > 1e-10 or abs(_pt.y - p.y) > 1e-10:
                _x, _y = self.__p2.getCoords()
                self.sendMessage('moved', _pt.x, _pt.y, _x, _y)
            self.modified()

    p1 = property(getP1, setP1, None, "First endpoint of the Segment.")

    def getP2(self):
        """Return the second endpoint Point of the Segment.

getP2()
        """
        return self.__p2

    def setP2(self, p):
        """Set the second endpoint Point of the Segment.

setP2(p)
        """
        if self.isLocked():
            raise RuntimeError, "Setting endpoint not allowed - object locked."
        if not isinstance(p, point.Point):
            raise TypeError, "Invalid Point for p2 endpoint: " + str(p)
        if p is self.__p1:
            raise ValueError, "Segments cannot have identical endpoints."
        _pt = self.__p2
        if _pt is not p:
            _pt.disconnect(self)
            _pt.freeUser(self)
            self.__p2 = p
            self.sendMessage('endpoint_changed', _pt, p)
            p.storeUser(self)
            p.connect('moved', self._movePoint)
            if abs(_pt.x - p.x) > 1e-10 or abs(_pt.y - p.y) > 1e-10:
                _x, _y = self.__p1.getCoords()
                self.sendMessage('moved', _x, _y, _pt.x, _pt.y)
            self.modified()

    p2 = property(getP2, setP2, None, "Second endpoint of the Segment.")

    def move(self, dx, dy):
        """Move a Segment.

move(dx, dy)

The first argument gives the x-coordinate displacement,
and the second gives the y-coordinate displacement. Both
values should be floats.
        """
        if self.isLocked() or self.__p1.isLocked() or self.__p2.isLocked():
            raise RuntimeError, "Moving Segment not allowed - object locked."
        _dx = util.get_float(dx)
        _dy = util.get_float(dy)
        if abs(_dx) > 1e-10 or abs(_dy) > 1e-10:
            _x1, _y1 = self.__p1.getCoords()
            _x2, _y2 = self.__p2.getCoords()
            self.ignore('moved')
            try:
                self.__p1.move(_dx, _dy)
                self.__p2.move(_dx, _dy)
            finally:
                self.receive('moved')
            self.sendMessage('moved', _x1, _y1, _x2, _y2)
            self.modified()

    def length(self):
        """Return the length of the Segment.

length()
        """
        return self.__p1 - self.__p2

    def getCoefficients(self):
        """Express the line segment as a function ax + by + c = 0

getCoefficients()

This method returns a tuple of three floats: (a, b, c)
        """
        _x1, _y1 = self.__p1.getCoords()
        _x2, _y2 = self.__p2.getCoords()
        _a = _y2 - _y1
        _b = _x1 - _x2
        _c = (_x2 * _y1) - (_x1 * _y2)
        return _a, _b, _c

    def getProjection(self, x, y):
        """Find the projection point of some coordinates on the Segment.

getProjection(x, y)

Arguments 'x' and 'y' should be float values.
        """
        _x = util.get_float(x)
        _y = util.get_float(y)
        _x1, _y1 = self.__p1.getCoords()
        _x2, _y2 = self.__p2.getCoords()
        _sqlen = pow((_x2 - _x1), 2) +  pow((_y2 - _y1), 2)
        if _sqlen < 1e-10: # coincident points
            return None
        _rn = ((_x - _x1) * (_x2 - _x1)) + ((_y - _y1) * (_y2 - _y1))
        _r = _rn/_sqlen
        if _r < 0.0 or _r > 1.0:
            return None
        _px = _x1 + _r * (_x2 - _x1)
        _py = _y1 + _r * (_y2 - _y1)
        return _px, _py

    def mapCoords(self, x, y, tol=tolerance.TOL):
        """Return the nearest Point on the Segment to a coordinate pair.

mapCoords(x, y[, tol])

The function has two required arguments:

x: A Float value giving the 'x' coordinate
y: A Float value giving the 'y' coordinate

There is a single optional argument:

tol: A float value equal or greater than 0.

This function is used to map a possibly near-by coordinate pair to an
actual Point on the Segment. If the distance between the actual
Point and the coordinates used as an argument is less than the tolerance,
the actual Point is returned. Otherwise, this function returns None.
        """
        _x = util.get_float(x)
        _y = util.get_float(y)
        _t = tolerance.toltest(tol)
        _x1, _y1 = self.__p1.getCoords()
        _x2, _y2 = self.__p2.getCoords()
        return util.map_coords(_x, _y, _x1, _y1, _x2, _y2, _t)

    def inRegion(self, xmin, ymin, xmax, ymax, fully=False):
        """Return whether or not a Segment exists within a region.

inRegion(xmin, ymin, xmax, ymax[, fully])

The four arguments define the boundary of an area, and the
method returns True if the Segment lies within that area. If
the optional argument fully is used and is True, then both
endpoints of the Segment must lie within the boundary.
Otherwise, the method returns False.
        """
        _xmin = util.get_float(xmin)
        _ymin = util.get_float(ymin)
        _xmax = util.get_float(xmax)
        if _xmax < _xmin:
            raise ValueError, "Illegal values: xmax < xmin"
        _ymax = util.get_float(ymax)
        if _ymax < _ymin:
            raise ValueError, "Illegal values: ymax < ymin"
        util.test_boolean(fully)
        _x1, _y1 = self.__p1.getCoords()
        _x2, _y2 = self.__p2.getCoords()
        _pxmin = min(_x1, _x2)
        _pymin = min(_y1, _y2)
        _pxmax = max(_x1, _x2)
        _pymax = max(_y1, _y2)
        if ((_pxmax < _xmin) or
            (_pymax < _ymin) or
            (_pxmin > _xmax) or
            (_pymin > _ymax)):
            return False
        if fully:
            if ((_pxmin > _xmin) and
                (_pymin > _ymin) and
                (_pxmax < _xmax) and
                (_pymax < _ymax)):
                return True
            return False
        return util.in_region(_x1, _y1, _x2, _y2, _xmin, _ymin, _xmax, _ymax)

    def clipToRegion(self, xmin, ymin, xmax, ymax):
        """Clip the Segment using the Liang-Barsky Algorithm.

clipToRegion(xmin, ymin, xmax, ymax)
        """
        _xmin = util.get_float(xmin)
        _ymin = util.get_float(ymin)
        _xmax = util.get_float(xmax)
        if _xmax < _xmin:
            raise ValueError, "Illegal values: xmax < xmin"
        _ymax = util.get_float(ymax)
        if _ymax < _ymin:
            raise ValueError, "Illegal values: ymax < ymin"
        _x1, _y1 = self.__p1.getCoords()
        _x2, _y2 = self.__p2.getCoords()
        #
        # simple tests to reject line
        #
        if ((max(_x1, _x2) < _xmin) or
            (max(_y1, _y2) < _ymin) or
            (min(_x1, _x2) > _xmax) or
            (min(_y1, _y2) > _ymax)):
            return None
        #
        # simple tests to accept line
        #
        _coords = None
        if (_xmin < _x1 < _xmax and
            _xmin < _x2 < _xmax and
            _ymin < _y1 < _ymax and
            _ymin < _y2 < _ymax):
            _coords = (_x1, _y1, _x2, _y2)
        else:
            #
            # the Segment can be parameterized as
            #
            # x = u * (x2 - x1) + x1
            # y = u * (y2 - y1) + y1
            #
            # for u = 0, x => x1, y => y1
            # for u = 1, x => x2, y => y2
            #
            # The following is the Liang-Barsky Algorithm
            # for segment clipping
            #
            _dx = _x2 - _x1
            _dy = _y2 - _y1
            _P = [-_dx, _dx, -_dy, _dy]
            _q = [(_x1 - _xmin), (_xmax - _x1), (_y1 - _ymin), (_ymax - _y1)]
            _u1 = 0.0
            _u2 = 1.0
            _valid = True
            for _i in range(4):
                _pi = _P[_i]
                _qi = _q[_i]
                if abs(_pi) < 1e-10:
                    if _qi < 0.0:
                        _valid = False
                        break
                else:
                    _r = _qi/_pi
                    if _pi < 0.0:
                        if _r > _u2:
                            _valid = False
                            break
                        if _r > _u1:
                            _u1 = _r
                    else:
                        if _r < _u1:
                            _valid = False
                            break
                        if _r < _u2:
                            _u2 = _r
            if _valid:
                _coords = (((_u1 * _dx) + _x1),
                           ((_u1 * _dy) + _y1),
                           ((_u2 * _dx) + _x1),
                           ((_u2 * _dy) + _y1))
        return _coords

    def _movePoint(self, p, *args):
        _alen = len(args)
        if _alen < 2:
            raise ValueError, "Invalid argument count: %d" % _alen
        _x = util.get_float(args[0])
        _y = util.get_float(args[1])
        _p1 = self.__p1
        _p2 = self.__p2
        if p is _p1:
            _x1 = _x
            _y1 = _y
            _x2, _y2 = _p2.getCoords()
        elif p is _p2:
            _x1, _y1 = _p1.getCoords()
            _x2 = _x
            _y2 = _y
        else:
            raise ValueError, "Unexpected Segment endpoint: " + `p`
        self.sendMessage('moved', _x1, _y1, _x2, _y2)

    def clone(self):
        """Create an identical copy of a Segment.

clone()
        """
        _cp1 = self.__p1.clone()
        _cp2 = self.__p2.clone()
        _st = self.getStyle()
        _lt = self.getLinetype()
        _col = self.getColor()
        _th = self.getThickness()
        return Segment(_cp1, _cp2, _st, _lt, _col, _th)

    def sendsMessage(self, m):
        if m in Segment.messages:
            return True
        return super(Segment, self).sendsMessage(m)

#
# Quadtree Segment storage
#

class SegmentQuadtree(quadtree.Quadtree):
    def __init__(self):
        super(SegmentQuadtree, self).__init__()

    def getNodes(self, *args):
        _alen = len(args)
        if _alen != 4:
            raise ValueError, "Expected 4 arguments, got %d" % _alen
        _x1 = util.get_float(args[0])
        _y1 = util.get_float(args[1])
        _x2 = util.get_float(args[2])
        _y2 = util.get_float(args[3])
        _sxmin = min(_x1, _x2)
        _sxmax = max(_x1, _x2)
        _symin = min(_y1, _y2)
        _symax = max(_y1, _y2)
        _nodes = [self.getTreeRoot()]
        while len(_nodes):
            _node = _nodes.pop()
            _xmin, _ymin, _xmax, _ymax = _node.getBoundary()
            if ((_sxmin > _xmax) or
                (_sxmax < _xmin) or
                (_symin > _ymax) or
                (_symax < _ymin)):
                continue
            if _node.hasSubnodes():
                _xmid = (_xmin + _xmax)/2.0
                _ymid = (_ymin + _ymax)/2.0
                _ne = _nw = _sw = _se = True
                if _sxmax < _xmid: # seg on left side
                    _ne = _se = False
                if _sxmin > _xmid: # seg on right side
                    _nw = _sw = False
                if _symax < _ymid: # seg below
                    _nw = _ne = False
                if _symin > _ymid: # seg above
                    _sw = _se = False
                if _ne:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.NENODE))
                if _nw:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.NWNODE))
                if _sw:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.SWNODE))
                if _se:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.SENODE))
            else:
                yield _node

    def addObject(self, obj):
        if not isinstance(obj, Segment):
            raise TypeError, "Invalid Segment object: " + `obj`
        if obj in self:
            return
        _p1, _p2 = obj.getEndpoints()
        _x1, _y1 = _p1.getCoords()
        _x2, _y2 = _p2.getCoords()
        _bounds = self.getTreeRoot().getBoundary()
        _xmin = _ymin = _xmax = _ymax = None
        _sxmin = min(_x1, _x2)
        _sxmax = max(_x1, _x2)
        _symin = min(_y1, _y2)
        _symax = max(_y1, _y2)
        _resize = False
        if _bounds is None: # first node in tree
            _resize = True
            _xmin = _sxmin - 1.0
            _ymin = _symin - 1.0
            _xmax = _sxmax + 1.0
            _ymax = _symax + 1.0
        else:
            _xmin, _ymin, _xmax, _ymax = _bounds
            if _sxmin < _xmin:
                _xmin = _sxmin - 1.0
                _resize = True
            if _sxmax > _xmax:
                _xmax = _sxmax + 1.0
                _resize = True
            if _symin < _ymin:
                _ymin = _symin - 1.0
                _resize = True
            if _symax > _ymax:
                _ymax = _symax + 1.0
                _resize = True
        if _resize:
            self.resize(_xmin, _ymin, _xmax, _ymax)
        for _node in self.getNodes(_x1, _y1, _x2, _y2):
            _xmin, _ymin, _xmax, _ymax = _node.getBoundary()
            if obj.inRegion(_xmin, _ymin, _xmax, _ymax):
                _node.addObject(obj)
        super(SegmentQuadtree, self).addObject(obj)
        obj.connect('moved', self._moveSegment)

    def delObject(self, obj):
        if obj not in self:
            return
        _p1, _p2 = obj.getEndpoints()
        _x1, _y1 = _p1.getCoords()
        _x2, _y2 = _p2.getCoords()
        _pdict = {}
        for _node in self.getNodes(_x1, _y1, _x2, _y2):
            _node.delObject(obj)
            _parent = _node.getParent()
            if _parent is not None:
                _pid = id(_parent)
                if _pid not in _pdict:
                    _pdict[_pid] = _parent
        super(SegmentQuadtree, self).delObject(obj)
        obj.disconnect(self)
        for _parent in _pdict.values():
            self.purgeSubnodes(_parent)
        #
        # test
        #
        _nodes = [self.getTreeRoot()]
        while len(_nodes):
            _node = _nodes.pop()
            if _node.hasSubnodes():
                _nodes.extend(_node.getSubnodes())
            else:
                for _obj in _node.getObjects():
                    if _obj is obj:
                        raise ValueError, "object still in tree" + `obj`

    def new_find(self, *args):
        _alen = len(args)
        if _alen < 4:
            raise ValueError, "Invalid argument count: %d" % _alen
        _x1 = args[0]
        if not isinstance(_x1, float):
            _x1 = float(args[0])
        _y1 = args[1]
        if not isinstance(_y1, float):
            _y1 = float(args[1])
        _x2 = args[2]
        if not isinstance(_x2, float):
            _x2 = float(args[2])
        _y2 = args[3]
        if not isinstance(_y2, float):
            _y2 = float(args[3])
        _t = tolerance.TOL
        if _alen > 4:
            _t = tolerance.toltest(args[4])
        if not len(self):
            return None
        _sdict = {}
        _segs = []
        for _node in self.getNodes(_x1, _y1, _x2, _y2):
            _xmin, _ymin, _xmax, _ymax = _node.getBoundary()
            for _s in _node.getObjects():
                _sid = id(_s)
                if _sid not in _sdict:
                    _p1, _p2 = _s.getEndpoints()
                    _px1, _py1 = _p1.getCoords()
                    _px2, _py2 = _p2.getCoords()
                    if ((abs(_px1 - _x1) < _t) and
                        (abs(_py1 - _y1) < _t) and
                        (abs(_px2 - _x2) < _t) and
                        (abs(_py2 - _y2) < _t)):
                        _segs.append(_s)
                    elif ((abs(_px2 - _x1) < _t) and
                          (abs(_py2 - _y1) < _t) and
                          (abs(_px1 - _x2) < _t) and
                          (abs(_py1 - _y2) < _t)):
                        _segs.append(_s)
                    _sdict[_sid] = True
        return _segs

    def find(self, *args):
        _alen = len(args)
        if _alen < 4:
            raise ValueError, "Invalid argument count: %d" % _alen
        _x1 = util.get_float(args[0])
        _y1 = util.get_float(args[1])
        _x2 = util.get_float(args[2])
        _y2 = util.get_float(args[3])
        _t = tolerance.TOL
        if _alen > 4:
            _t = tolerance.toltest(args[4])
        if not len(self):
            return None
        _sxmin = min(_x1, _x2)
        _sxmax = max(_x1, _x2)
        _symin = min(_y1, _y2)
        _symax = max(_y1, _y2)
        _nodes = [self.getTreeRoot()]
        _sdict = {}
        _bailout = False
        _seg = None
        while len(_nodes):
            _node = _nodes.pop()
            _xmin, _ymin, _xmax, _ymax = _node.getBoundary()
            if ((_sxmin > _xmax) or
                (_sxmax < _xmin) or
                (_symin > _ymax) or
                (_symax < _ymin)):
                continue
            if _node.hasSubnodes():
                _xmid = (_xmin + _xmax)/2.0
                _ymid = (_ymin + _ymax)/2.0
                _ne = _nw = _sw = _se = True
                if _sxmax < (_xmid - _t): # seg on left side
                    _ne = _se = False
                if _sxmin > (_xmid + _t): # seg on right side
                    _nw = _sw = False
                if _symax < (_ymid - _t): # seg below
                    _nw = _ne = False
                if _symin > (_ymid + _t): # seg above
                    _sw = _se = False
                if _ne:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.NENODE))
                if _nw:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.NWNODE))
                if _sw:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.SWNODE))
                if _se:
                    _nodes.append(_node.getSubnode(quadtree.QTreeNode.SENODE))
            else:
                for _s in _node.getObjects():
                    _sid = id(_s)
                    if _sid not in _sdict:
                        _p1, _p2 = _s.getEndpoints()
                        _px, _py = _p1.getCoords()
                        if ((abs(_px - _x1) < _t) and (abs(_py - _y1) < _t)):
                            _px, _py = _p2.getCoords()
                            if ((abs(_px - _x2) < _t) and
                                (abs(_py - _y2) < _t)):
                                _bailout = True
                                _seg = _s
                                break
                        _px, _py = _p2.getCoords()
                        if ((abs(_px - _x1) < _t) and (abs(_py - _y1) < _t)):
                            _px, _py = _p1.getCoords()
                            if ((abs(_px - _x2) < _t) and
                                (abs(_py - _y2) < _t)):
                                _bailout = True
                                _seg = _s
                                break
                        _sdict[_sid] = True
            if _bailout:
                break
        return _seg

    def _moveSegment(self, obj, *args):
        if obj not in self:
            raise ValueError, "Segment not stored in Quadtree: " + `obj`
        _alen = len(args)
        if _alen < 4:
            raise ValueError, "Invalid argument count: %d" % _alen
        _x1 = util.get_float(args[0])
        _y1 = util.get_float(args[1])
        _x2 = util.get_float(args[2])
        _y2 = util.get_float(args[3])
        for _node in self.getNodes(_x1, _y1, _x2, _y2):
            _node.delObject(obj) # segment may not be in node ...
        super(SegmentQuadtree, self).delObject(obj)
        obj.disconnect(self)
        self.addObject(obj)

    def getClosest(self, x, y, tol=tolerance.TOL):
        _x = util.get_float(x)
        _y = util.get_float(y)
        _t = tolerance.toltest(tol)
        _seg = _tsep = None
        _bailout = False
        _sdict = {}
        _nodes = [self.getTreeRoot()]
        while len(_nodes):
            _node = _nodes.pop()
            _xmin, _ymin, _xmax, _ymax = _node.getBoundary()
            if ((_x < (_xmin - _t)) or
                (_x > (_xmax + _t)) or
                (_y < (_ymin - _t)) or
                (_y > (_ymax + _t))):
                continue
            if _node.hasSubnodes():
                _nodes.extend(_node.getSubnodes())
            else:
                for _s in _node.getObjects():
                    _sid = id(_s)
                    if _sid not in _sdict:
                        _p1, _p2 = _s.getEndpoints()
                        _px, _py = _p1.getCoords()
                        if ((abs(_px - _x) < 1e-10) and
                            (abs(_py - _y) < 1e-10)):
                            _seg = _s
                            _bailout = True
                            break
                        _px, _py = _p2.getCoords()
                        if ((abs(_px - _x) < 1e-10) and
                            (abs(_py - _y) < 1e-10)):
                            _seg = _s
                            _bailout = True
                            break
                        _sdict[_sid] = True
                    _pt = _s.mapCoords(_x, _y, _t)
                    if _pt is not None:
                        _px, _py = _pt
                        _sep = math.hypot((_px - _x), (_py - _y))
                        if _tsep is None:
                            _tsep = _sep
                            _seg = _s
                        else:
                            if _sep < _tsep:
                                _tsep = _sep
                                _seg = _s
            if _bailout:
                break
        return _seg

    def getInRegion(self, xmin, ymin, xmax, ymax):
        _xmin = util.get_float(xmin)
        _ymin = util.get_float(ymin)
        _xmax = util.get_float(xmax)
        if _xmax < _xmin:
            raise ValueError, "Illegal values: xmax < xmin"
        _ymax = util.get_float(ymax)
        if _ymax < _ymin:
            raise ValueError, "Illegal values: ymax < ymin"
        _segs = []
        if not len(self):
            return _segs
        _nodes = [self.getTreeRoot()]
        _sdict = {}
        while len(_nodes):
            _node = _nodes.pop()
            if _node.hasSubnodes():
                for _subnode in _node.getSubnodes():
                    _sxmin, _symin, _sxmax, _symax = _subnode.getBoundary()
                    if ((_sxmin > _xmax) or
                        (_symin > _ymax) or
                        (_sxmax < _xmin) or
                        (_symax < _ymin)):
                        continue
                    _nodes.append(_subnode)
            else:
                for _seg in _node.getObjects():
                    _sid = id(_seg)
                    if _sid not in _sdict:
                        if _seg.inRegion(_xmin, _ymin, _xmax, _ymax):
                            _segs.append(_seg)
                        _sdict[_sid] = True
        return _segs

#
# Segment history class
#

class SegmentLog(graphicobject.GraphicObjectLog):
    def __init__(self, s):
        if not isinstance(s, Segment):
            raise TypeError, "Invalid segment: " + `s`
        super(SegmentLog, self).__init__(s)
        s.connect('endpoint_changed', self._endpointChange)

    def _endpointChange(self, s, *args):
        _alen = len(args)
        if _alen < 2:
            raise ValueError, "Invalid argument count: %d" % _alen
        _old = args[0]
        if not isinstance(_old, point.Point):
            raise TypeError, "Invalid old endpoint: " + `_old`
        _new = args[1]
        if not isinstance(_new, point.Point):
            raise TypeError, "Invalid new endpoint: " + `_new`
        self.saveUndoData('endpoint_changed', _old.getID(), _new.getID())

    def execute(self, undo, *args):
        util.test_boolean(undo)
        _alen = len(args)
        if len(args) == 0:
            raise ValueError, "No arguments to execute()"
        _s = self.getObject()
        _p1, _p2 = _s.getEndpoints()
        _op = args[0]
        if _op == 'endpoint_changed':
            if _alen < 3:
                raise ValueError, "Invalid argument count: %d" % _alen
            _oid = args[1]
            _nid = args[2]
            _parent = _s.getParent()
            if _parent is None:
                raise ValueError, "Segment has no parent - cannot undo"
            self.ignore(_op)
            try:
                if undo:
                    _pt = _parent.getObject(_oid)
                    if _pt is None or not isinstance(_pt, point.Point):
                        raise ValueError, "Old endpoint missing: id=%d" % _oid
                    _s.startUndo()
                    try:
                        if _p1.getID() == _nid:
                            _s.setP1(_pt)
                        elif _p2.getID() == _nid:
                            _s.setP2(_pt)
                        else:
                            raise ValueError, "Unexpected endpoint ID: %d" % _nid
                    finally:
                        _s.endUndo()
                else:
                    _pt = _parent.getObject(_nid)
                    if _pt is None or not isinstance(_pt, point.Point):
                        raise ValueError, "New endpoint missing: id=%d" % _nid
                    _s.startRedo()
                    try:
                        if _p1.getID() == _oid:
                            _s.setP1(_pt)
                        elif _p2.getID() == _oid:
                            _s.setP2(_pt)
                        else:
                            raise ValueError, "Unexpected endpoint ID: %d" % _oid
                    finally:
                        _s.endRedo()
            finally:
                self.receive(_op)
            self.saveData(undo, _op, _oid, _nid)
        else:
            super(SegmentLog, self).execute(undo, *args)
