#
# Copyright (c) 2002, 2003, 2004 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

import graphicobject
import entity
import tolerance
import style
import linetype
import color
import point
import util
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
        graphicobject.GraphicObject.__init__(self, _st, lt, col, th, **kw)
        self.__p1 = _p1
        _p1.connect('moved', self, Segment.movePoint)
        _p1.storeUser(self)
        self.__p2 = _p2
        _p2.connect('moved', self, Segment.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 getValues(self):
        _pids = entity.Entity.getValues(self)
        _gv = self.getGraphicValues(Segment.__defstyle)
        return 'segment', _pids, _gv, self.__p1.getID(), self.__p2.getID()

    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, Segment.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, Segment.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 = dx
        if not isinstance(_dx, float):
            _dx = float(dx)
        _dy = dy
        if not isinstance(_dy, float):
            _dy = 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 = x
        if not isinstance(_x, float):
            _x = float(x)
        _y = y
        if not isinstance(_y, float):
            _y = 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 = x
        if not isinstance(_x, float):
            _x = float(x)
        _y = y
        if not isinstance(_y, float):
            _y = 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 = xmin
        if not isinstance(_xmin, float):
            _xmin = float(xmin)
        _ymin = ymin
        if not isinstance(_ymin, float):
            _ymin = float(ymin)
        _xmax = xmax
        if not isinstance(_xmax, float):
            _xmax = float(xmax)
        if _xmax < _xmin:
            raise ValueError, "Illegal values: xmax < xmin"
        _ymax = ymax
        if not isinstance(_ymax, float):
            _ymax = float(ymax)
        if _ymax < _ymin:
            raise ValueError, "Illegal values: ymax < ymin"
        if fully is not True and fully is not False:
            raise ValueError, "Invalid flag: " + str(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 = xmin
        if not isinstance(_xmin, float):
            _xmin = float(xmin)
        _ymin = ymin
        if not isinstance(_ymin, float):
            _ymin = float(ymin)
        _xmax = xmax
        if not isinstance(_xmax, float):
            _xmax = float(xmax)
        if _xmax < _xmin:
            raise ValueError, "Illegal values: xmax < xmin"
        _ymax = ymax
        if not isinstance(_ymax, float):
            _ymax = 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 = args[0]
        if not isinstance(_x, float):
            _x = float(args[0])
        _y = args[1]
        if not isinstance(_y, float):
            _y = 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)
        self.modified()

    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 graphicobject.GraphicObject.sendsMessage(self, m)

#
# Quadtree Segment storage
#

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

    def getNodes(self, *args):
        _alen = len(args)
        if _alen != 4:
            raise ValueError, "Expected 4 arguments, got %d" % _alen
        _x1, _y1, _x2, _y2 = args
        if not isinstance(_x1, float):
            _x1 = float(args[0])
        if not isinstance(_y1, float):
            _y1 = float(args[1])
        if not isinstance(_x2, float):
            _x2 = float(args[2])
        if not isinstance(_y2, float):
            _y2 = 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)
        quadtree.Quadtree.addObject(self, obj)
        obj.connect('moved', self, SegmentQuadtree.moveSegment)

    def old_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()
        _node = self.getTreeRoot()
        _bounds = _node.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)
            _node = self.getTreeRoot()
        _nodes = [_node]
        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:
                if obj.inRegion(_xmin, _ymin, _xmax, _ymax):
                    _node.addObject(obj)
        quadtree.Quadtree.addObject(self, obj)
        obj.connect('moved', self, SegmentQuadtree.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
        quadtree.Quadtree.delObject(self, obj)
        obj.disconnect(self)
        for _parent in _pdict.values():
            self.purgeSubnodes(_parent)

    def old_delObject(self, obj):
        if obj not in self:
            return
        _p1, _p2 = obj.getEndpoints()
        _x1, _y1 = _p1.getCoords()
        _x2, _y2 = _p2.getCoords()
        _xmin = _ymin = _xmax = _ymax = None
        _sxmin = min(_x1, _x2)
        _sxmax = max(_x1, _x2)
        _symin = min(_y1, _y2)
        _symax = max(_y1, _y2)
        _pdict = {}
        _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:
                _node.delObject(obj) # seg may not be in the node ...
                _parent = _node.getParent()
                if _parent is not None:
                    _pid = id(_parent)
                    if _pid not in _pdict:
                        _pdict[_pid] = _parent
        quadtree.Quadtree.delObject(self, obj)
        obj.disconnect(self)
        for _parent in _pdict.values():
            self.purgeSubnodes(_parent)

    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 = 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
        _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 = 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])
        for _node in self.getNodes(_x1, _y1, _x2, _y2):
            _node.delObject(obj) # segment may not be in node ...
        quadtree.Quadtree.delObject(self, obj)
        obj.disconnect(self)
        self.addObject(obj)

    def old_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 = 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])
        _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:
                _node.delObject(obj) # segment may not be in node ...
        quadtree.Quadtree.delObject(self, obj)
        obj.disconnect(self)
        self.addObject(obj)

    def getClosest(self, x, y, tol=tolerance.TOL):
        _x = x
        if not isinstance(_x, float):
            _x = float(x)
        _y = y
        if not isinstance(_y, float):
            _y = 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 = xmin
        if not isinstance(_xmin, float):
            _xmin = float(xmin)
        _ymin = ymin
        if not isinstance(_ymin, float):
            _ymin = float(ymin)
        _xmax = xmax
        if not isinstance(_xmax, float):
            _xmax = float(xmax)
        if _xmax < _xmin:
            raise ValueError, "Illegal values: xmax < xmin"
        _ymax = ymax
        if not isinstance(_ymax, float):
            _ymax = float(ymax)
        if _ymax < _ymin:
            raise ValueError, "Illegal values: ymax < ymin"
        _segs = []
        if not len(self):
            return _segs
        _nodes = []
        _nodes.append(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`
        graphicobject.GraphicObjectLog.__init__(self, s)
        s.connect('endpoint_changed', self, SegmentLog.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`
        _oid = _old.getID()
        _new = args[1]
        if not isinstance(_new, point.Point):
            raise TypeError, "Invalid new endpoint: " + `_new`
        _nid = _new.getID()
        self.saveUndoData('endpoint_changed', _oid, _nid)

    def execute(self, undo, *args):
        if undo is not True and undo is not False:
            raise ValueError, "Invalid undo value: " + str(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]
            if undo:
                self.saveRedoData('endpoint_changed', _oid, _nid)
                self.ignore('endpoint_changed')
                try:
                    _parent = _s.getParent()
                    if _parent is None:
                        raise ValueError, "Segment has no parent - cannot undo"
                    _op = _parent.getObject(_oid)
                    if _op is None or not isinstance(_op, point.Point):
                        raise ValueError, "Old endpoint missing: id=%d" % _oid
                    _x1, _y1 = _p1.getCoords()
                    _x2, _y2 = _p2.getCoords()
                    _moved = False
                    _s.startUndo(True)
                    try:
                        if _p1.getID() == _nid:
                            _s.setP1(_op)
                            if (abs(_op.x - _x1) > 1e-10 or
                                abs(_op.y - _y1) > 1e-10):
                                _moved = True
                        elif _p2.getID() == _nid:
                            _s.setP2(_op)
                            if (abs(_op.x - _x2) > 1e-10 or
                                abs(_op.y - _y2) > 1e-10):
                                _moved = True
                        else:
                            raise ValueError, "Unexpected endpoint ID: %d" % _nid
                    finally:
                        _s.endUndo()
                    if _moved:
                        _s.sendMessage('moved', _x1, _y1, _x2, _y2)
                finally:
                    self.receive('endpoint_changed')
            else:
                self.saveUndoData('endpoint_changed', _oid, _nid)
                _parent = _s.getParent()
                if _parent is None:
                    raise ValueError, "Segment has no parent - cannot undo"
                _np = _parent.getObject(_nid)
                if _np is None or not isinstance(_np, point.Point):
                    raise ValueError, "New endpoint missing: id=%d" % _nid
                self.ignore('endpoint_changed')
                try:
                    if _p1.getID() == _oid:
                        _s.setP1(_np)
                    elif _p2.getID() == _oid:
                        _s.setP2(_np)
                    else:
                        raise ValueError, "Unexpected endpoint ID: %d" % _oid
                finally:
                    self.receive('endpoint_changed')
        else:
            raise ValueError, "Unexpected operation: %s" % _op
