diff --git a/core/annotations.cpp b/core/annotations.cpp index 353759b27..132d38868 100644 --- a/core/annotations.cpp +++ b/core/annotations.cpp @@ -14,6 +14,9 @@ #include #include +// DBL_MAX +#include + // local includes #include "action.h" #include "document.h" @@ -24,6 +27,65 @@ using namespace Okular; +/** + * True, if point @p c lies to the left of the vector from @p a to @p b + * @internal + */ +static bool isLeftOfVector( const NormalizedPoint& a, const NormalizedPoint& b, const NormalizedPoint& c ) +{ + //cross product + return ( (b.x - a.x) * ( c.y - a.y) - ( b.y - a.y ) * ( c.x - a.x ) ) > 0; +} + +/** + * @brief Calculates distance of the given point @p x @p y @p xScale @p yScale to the @p path + * + * Does piecewise comparison and selects the distance to the closest segment + */ +static double distanceSqr( double x, double y, double xScale, double yScale, const QLinkedList& path ) +{ + double distance = DBL_MAX; + double thisDistance; + QLinkedList::const_iterator i = path.constBegin(); + NormalizedPoint lastPoint = *i; + + for (++i; i != path.constEnd(); ++i) { + thisDistance = NormalizedPoint::distanceSqr( x, y, xScale, yScale, lastPoint, (*i) ); + + if ( thisDistance < distance ) + distance = thisDistance; + + lastPoint = *i; + } + return distance; +} + +/** + * Given the squared @p distance from the idealized 0-width line and a pen width @p penWidth, + * (not squared!), returns the final distance + * + * @warning The returned distance is not exact: + * We calculate an (exact) squared distance to the ideal (centered) line, and then substract + * the squared width of the pen: + * a^2 - b^2 where a = "distance from idealized 0-width line" b = "pen width" + * For an exact result, we would want to calculate "(a - b)^2" but that would require + * a square root operation because we only know the squared distance a^2. + * + * However, the approximation is feasible, because: + * error = (a-b)^2 - (a^2 - b^2) = -2ab + 2b^2 = 2b(b - a) + * Therefore: + * lim_{a->b} a^2 - b^2 - a^2 + 2ab - b^2 --> 0 + * + * In other words, this approximation will estimate the distance to be slightly more than it actually is + * for as long as we are far "outside" the line, becoming more accurate the closer we get to the line + * boundary. Trivially, it also fullfills (a1 < a2) => ((a1^2 - b^2) < (a2^2 - b^2)) making it monotonic. + * "Inside" of the drawn line, the distance is 0 anyway. + */ +static double strokeDistance( double distance, double penWidth ) +{ + return fmax(distance - pow( penWidth, 2 ), 0); +} + //BEGIN AnnotationUtils implementation Annotation * AnnotationUtils::createAnnotation( const QDomElement & annElement ) { @@ -771,6 +833,11 @@ void Annotation::setAnnotationProperties( const QDomNode& node ) d_ptr->transform( d_ptr->m_page->rotationMatrix() ); } +double AnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) +{ + return m_transformedBoundary.distanceSqr( x, y, xScale, yScale ); +} + void AnnotationPrivate::annotationTransform( const QTransform &matrix ) { resetTransformation(); @@ -1200,6 +1267,7 @@ class Okular::LineAnnotationPrivate : public Okular::AnnotationPrivate virtual void baseTransform( const QTransform &matrix ); virtual void resetTransformation(); virtual void translate( const NormalizedPoint &coord ); + virtual double distanceSqr( double x, double y, double xScale, double yScale ); virtual void setAnnotationProperties( const QDomNode& node ); virtual AnnotationPrivate* getNewAnnotationPrivate(); @@ -1490,6 +1558,12 @@ AnnotationPrivate* LineAnnotationPrivate::getNewAnnotationPrivate() return new LineAnnotationPrivate(); } +double LineAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) +{ + return strokeDistance( ::distanceSqr( x, y, xScale, yScale, m_transformedLinePoints ), + m_style.width() * xScale / ( m_page->m_width * 2 ) ); +} + /** GeomAnnotation [Annotation] */ class Okular::GeomAnnotationPrivate : public Okular::AnnotationPrivate @@ -1501,6 +1575,7 @@ class Okular::GeomAnnotationPrivate : public Okular::AnnotationPrivate } virtual void setAnnotationProperties( const QDomNode& node ); virtual AnnotationPrivate* getNewAnnotationPrivate(); + virtual double distanceSqr( double x, double y, double xScale, double yScale ); GeomAnnotation::GeomType m_geomType; QColor m_geomInnerColor; @@ -1597,6 +1672,75 @@ AnnotationPrivate* GeomAnnotationPrivate::getNewAnnotationPrivate() return new GeomAnnotationPrivate(); } +double GeomAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) +{ + double distance = 0; + //the line thickness is applied unevenly (only on the "inside") - account for this + bool withinShape = false; + switch (m_geomType) { + case GeomAnnotation::InscribedCircle: + { + //calculate the center point and focus lengths of the ellipse + const double centerX = ( m_transformedBoundary.left + m_transformedBoundary.right ) / 2.0; + const double centerY = ( m_transformedBoundary.top + m_transformedBoundary.bottom ) / 2.0; + const double focusX = ( m_transformedBoundary.right - centerX); + const double focusY = ( m_transformedBoundary.bottom - centerY); + + const double focusXSqr = pow( focusX, 2 ); + const double focusYSqr = pow( focusY, 2 ); + + // to calculate the distance from the ellipse, we will first find the point "projection" + // that lies on the ellipse and is closest to the point (x,y) + // This point can obviously be written as "center + lambda(inputPoint - center)". + // Because the point lies on the ellipse, we know that: + // 1 = ((center.x - projection.x)/focusX)^2 + ((center.y - projection.y)/focusY)^2 + // After filling in projection.x = center.x + lambda * (inputPoint.x - center.x) + // and its y-equivalent, we can solve for lambda: + const double lambda = sqrt( focusXSqr * focusYSqr / + ( focusYSqr * pow( x - centerX, 2 ) + focusXSqr * pow( y - centerY, 2 ) ) ); + + // if the ellipse is filled, we treat all points within as "on" it + if ( lambda > 1 ) + { + if ( m_geomInnerColor.isValid() ) + return 0; + else + withinShape = true; + } + + //otherwise we calculate the squared distance from the projected point on the ellipse + NormalizedPoint projection( centerX, centerY ); + projection.x += lambda * ( x - centerX ); + projection.y += lambda * ( y - centerY ); + + distance = projection.distanceSqr( x, y, xScale, yScale ); + break; + } + + case GeomAnnotation::InscribedSquare: + //if the square is filled, only check the bounding box + if ( m_geomInnerColor.isValid() ) + return AnnotationPrivate::distanceSqr( x, y, xScale, yScale ); + + QLinkedList edges; + edges << NormalizedPoint( m_transformedBoundary.left, m_transformedBoundary.top ); + edges << NormalizedPoint( m_transformedBoundary.right, m_transformedBoundary.top ); + edges << NormalizedPoint( m_transformedBoundary.right, m_transformedBoundary.bottom ); + edges << NormalizedPoint( m_transformedBoundary.left, m_transformedBoundary.bottom ); + edges << NormalizedPoint( m_transformedBoundary.left, m_transformedBoundary.top ); + distance = ::distanceSqr( x, y, xScale, yScale, edges ); + + if ( m_transformedBoundary.contains( x, y ) ) + withinShape = true; + + break; + } + if ( withinShape ) + distance = strokeDistance( distance, m_style.width() * xScale / m_page->m_width ); + + return distance; +} + /** HighlightAnnotation [Annotation] */ class HighlightAnnotation::Quad::Private @@ -1710,6 +1854,7 @@ class Okular::HighlightAnnotationPrivate : public Okular::AnnotationPrivate virtual void transform( const QTransform &matrix ); virtual void baseTransform( const QTransform &matrix ); + virtual double distanceSqr( double x, double y, double xScale, double yScale ); virtual void setAnnotationProperties( const QDomNode& node ); virtual AnnotationPrivate* getNewAnnotationPrivate(); @@ -1860,6 +2005,37 @@ AnnotationPrivate* HighlightAnnotationPrivate::getNewAnnotationPrivate() return new HighlightAnnotationPrivate(); } +double HighlightAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) +{ + NormalizedPoint point( x, y ); + double outsideDistance = DBL_MAX; + foreach ( const HighlightAnnotation::Quad& quad, m_highlightQuads ) + { + QLinkedList pathPoints; + + //first, we check if the point is within the area described by the 4 quads + //this is the case, if the point is always on one side of each segments delimiting the polygon: + pathPoints << NormalizedPoint( quad.point(0).x, quad.point(0).y ); + int directionVote = 0; + for ( int i = 1; i < 5; ++i ) + { + NormalizedPoint thisPoint( quad.point( i % 4 ).x, quad.point( i % 4 ).y ); + directionVote += (isLeftOfVector( pathPoints.back(), thisPoint, point )) ? 1 : -1; + pathPoints << thisPoint; + } + if ( abs( directionVote ) == 4 ) + return 0; + + //if that's not the case, we treat the outline as path and simply determine + //the distance from the path to the point + const double thisOutsideDistance = ::distanceSqr( x, y, xScale, yScale, pathPoints ); + if ( thisOutsideDistance < outsideDistance ) + outsideDistance = thisOutsideDistance; + } + + return outsideDistance; +} + /** StampAnnotation [Annotation] */ class Okular::StampAnnotationPrivate : public Okular::AnnotationPrivate @@ -1961,6 +2137,7 @@ class Okular::InkAnnotationPrivate : public Okular::AnnotationPrivate virtual void transform( const QTransform &matrix ); virtual void baseTransform( const QTransform &matrix ); virtual void resetTransformation(); + virtual double distanceSqr( double x, double y, double xScale, double yScale ); virtual void translate( const NormalizedPoint &coord ); virtual void setAnnotationProperties( const QDomNode& node ); virtual AnnotationPrivate* getNewAnnotationPrivate(); @@ -2038,6 +2215,18 @@ void InkAnnotation::store( QDomNode & node, QDomDocument & document ) const } } +double InkAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) +{ + double distance = DBL_MAX; + foreach ( const QLinkedList& path, m_transformedInkPaths ) + { + const double thisDistance = ::distanceSqr( x, y, xScale, yScale, path ); + if ( thisDistance < distance ) + distance = thisDistance; + } + return strokeDistance( distance, m_style.width() * xScale / ( m_page->m_width * 2 ) ); +} + void InkAnnotationPrivate::transform( const QTransform &matrix ) { AnnotationPrivate::transform( matrix ); diff --git a/core/annotations.h b/core/annotations.h index 79fd96539..4f107440d 100644 --- a/core/annotations.h +++ b/core/annotations.h @@ -93,6 +93,7 @@ class OKULAR_EXPORT Annotation friend class AnnotationObjectRect; friend class Document; friend class DocumentPrivate; + friend class ObjectRect; friend class Page; friend class PagePrivate; /// @endcond diff --git a/core/annotations_p.h b/core/annotations_p.h index f9a342fbb..07b124a4f 100644 --- a/core/annotations_p.h +++ b/core/annotations_p.h @@ -46,6 +46,13 @@ class AnnotationPrivate virtual void setAnnotationProperties( const QDomNode& node ); virtual AnnotationPrivate* getNewAnnotationPrivate() = 0; + /** + * Determines the distance of the closest point of the annotation to the + * given point @p x @p y @p xScale @p yScale + * @since 0.17 + */ + virtual double distanceSqr( double x, double y, double xScale, double yScale ); + PagePrivate * m_page; QString m_author; diff --git a/core/area.cpp b/core/area.cpp index d772fc0df..bf2eb7093 100644 --- a/core/area.cpp +++ b/core/area.cpp @@ -49,6 +49,72 @@ void NormalizedPoint::transform( const QTransform &matrix ) y = tmp_y; } +double NormalizedPoint::distanceSqr( double x, double y, double xScale, double yScale ) const +{ + return pow( (this->x - x) * xScale, 2 ) + pow( (this->y - y) * yScale, 2 ); +} + +/** + * Returns a vector from the given points @p a and @p b + * @internal + */ +NormalizedPoint operator-( const NormalizedPoint& a, const NormalizedPoint& b ) +{ + return NormalizedPoint( a.x - b.x, a.y - b.y ); +} + +/** + * @brief Calculates distance of the point @p x @p y @p xScale @p yScale to the line segment from @p start to @p end + */ +double NormalizedPoint::distanceSqr( double x, double y, double xScale, double yScale, const NormalizedPoint& start, const NormalizedPoint& end ) +{ + NormalizedPoint point( x, y ); + double thisDistance; + NormalizedPoint lineSegment( end - start ); + const double lengthSqr = pow( lineSegment.x, 2 ) + pow( lineSegment.y, 2 ); + + //if the length of the current segment is null, we can just + //measure the distance to either end point + if ( lengthSqr == 0.0 ) + { + thisDistance = end.distanceSqr( x, y, xScale, yScale ); + } + else + { + //vector from the start point of the current line segment to the measurement point + NormalizedPoint a = point - start; + //vector from the same start point to the end point of the current line segment + NormalizedPoint b = end - start; + + //we're using a * b (dot product) := |a| * |b| * cos(phi) and the knowledge + //that cos(phi) is adjacent side / hypotenuse (hypotenuse = |b|) + //therefore, t becomes the length of the vector that represents the projection of + //the point p onto the current line segment + //(hint: if this is still unclear, draw it!) + float t = (a.x * b.x + a.y * b.y) / lengthSqr; + + if ( t < 0 ) + { + //projection falls outside the line segment on the side of "start" + thisDistance = point.distanceSqr( start.x, start.y, xScale, yScale ); + } + else if ( t > 1 ) + { + //projection falls outside the line segment on the side of the current point + thisDistance = point.distanceSqr( end.x, end.y, xScale, yScale ); + } + else + { + //projection is within [start, *i]; + //determine the length of the perpendicular distance from the projection to the actual point + NormalizedPoint direction = end - start; + NormalizedPoint projection = start - NormalizedPoint( -t * direction.x, -t * direction.y ); + thisDistance = projection.distanceSqr( x, y, xScale, yScale ); + } + } + return thisDistance; +} + QDebug operator<<( QDebug str, const Okular::NormalizedPoint& p ) { str.nospace() << "NormPt(" << p.x << "," << p.y << ")"; @@ -316,27 +382,29 @@ double ObjectRect::distanceSqr( double x, double y, double xScale, double yScale { case Action: case Image: + { + const QRectF& rect( m_transformedPath.boundingRect() ); + return NormalizedRect( rect.x(), rect.y(), rect.right(), rect.bottom() ).distanceSqr( x, y, xScale, yScale ); + } case OAnnotation: { - const QPointF center = m_transformedPath.boundingRect().center(); - return pow( ( x - center.x() ), 2 ) + pow( ( y - center.y() ) * xScale / yScale, 2 ); + return static_cast(m_object)->d_func()->distanceSqr( x, y, xScale, yScale ); } case SourceRef: { - const double ratio = yScale / xScale; const SourceRefObjectRect * sr = static_cast< const SourceRefObjectRect * >( this ); const NormalizedPoint& point = sr->m_point; if ( point.x == -1.0 ) { - return pow( ( y - point.y ) / ratio, 2 ); + return pow( ( y - point.y ) * yScale, 2 ); } else if ( point.y == -1.0 ) { - return pow( ( x - point.x ), 2 ); + return pow( ( x - point.x ) * xScale, 2 ); } else { - return pow( ( x - point.x ), 2 ) + pow( ( y - point.y ) / ratio, 2 ); + return pow( ( x - point.x ) * xScale, 2 ) + pow( ( y - point.y ) * yScale, 2 ); } } } diff --git a/core/area.h b/core/area.h index 4f63759bd..1f7b10d2a 100644 --- a/core/area.h +++ b/core/area.h @@ -15,6 +15,7 @@ #include #include #include +#include #include "global.h" #include "okular_export.h" @@ -72,6 +73,19 @@ class OKULAR_EXPORT NormalizedPoint */ void transform( const QTransform &matrix ); + /** + * Returns squared distance to point @p x @p y @p xScale @p yScale + * @since 0.17 (KDE 4.11) + */ + double distanceSqr( double x, double y, double xScale, double yScale ) const; + + + /** + * @brief Calculates distance of the point @p x @p y @p xScale @p yScale to the line segment from @p start to @p end + * @since 0.17 (KDE 4.11) + */ + static double distanceSqr( double x, double y, double xScale, double yScale, const NormalizedPoint& start, const NormalizedPoint& end ); + /** * The normalized x coordinate. */ @@ -83,6 +97,7 @@ class OKULAR_EXPORT NormalizedPoint double y; }; + /** * NormalizedRect is a helper class which stores the coordinates * of a normalized rect, which is a rectangle of @see NormalizedPoints. @@ -263,6 +278,27 @@ class OKULAR_EXPORT NormalizedRect return right > pt.x; } + /** + * Returns the distance of the point @p x @p y @p xScale @p yScale to the closest + * edge or 0 if the point is within the rectangle + * @since 0.17 (KDE 4.11) + */ + double distanceSqr(double x, double y, double xScale, double yScale) const + { + double distX = 0; + if ( x < left ) + distX = left - x; + else if ( x > right ) + distX = x - right; + + double distY = 0; + if ( top > y ) + distY = top - y; + else if (bottom < y) + distY = y - bottom; + return pow( distX * xScale, 2 ) + pow( distY * yScale, 2 ); + } + /** * The normalized left coordinate. */ diff --git a/core/page.cpp b/core/page.cpp index a6fa6237c..37b0cd616 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -49,6 +49,8 @@ using namespace Okular; +static const double distanceConsideredEqual = 25; // 5px + static void deleteObjectRects( QLinkedList< ObjectRect * >& rects, const QSet& which ) { QLinkedList< ObjectRect * >::iterator it = rects.begin(), end = rects.end(); @@ -267,7 +269,7 @@ bool Page::hasObjectRect( double x, double y, double xScale, double yScale ) con QLinkedList< ObjectRect * >::const_iterator it = m_rects.begin(), end = m_rects.end(); for ( ; it != end; ++it ) - if ( (*it)->contains( x, y, xScale, yScale ) ) + if ( (*it)->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) return true; return false; @@ -430,7 +432,7 @@ const ObjectRect * Page::objectRect( ObjectRect::ObjectType type, double x, doub { QLinkedList< ObjectRect * >::const_iterator it = m_rects.begin(), end = m_rects.end(); for ( ; it != end; ++it ) - if ( ( (*it)->objectType() == type ) && (*it)->contains( x, y, xScale, yScale ) ) + if ( ( (*it)->objectType() == type ) && (*it)->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) return *it; return 0; } @@ -441,7 +443,7 @@ QLinkedList< const ObjectRect * > Page::objectRects( ObjectRect::ObjectType type QLinkedList< ObjectRect * >::const_iterator it = m_rects.begin(), end = m_rects.end(); for ( ; it != end; ++it ) - if ( ( (*it)->objectType() == type ) && (*it)->contains( x, y, xScale, yScale ) ) + if ( ( (*it)->objectType() == type ) && (*it)->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) result.append( *it ); return result; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 63c5c3540..116307f39 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,9 @@ target_link_libraries( parttest ${KDE4_KDECORE_LIBS} ${KDE4_KPARTS_LIBS} ${QT_QT kde4_add_unit_test( searchtest searchtest.cpp ) target_link_libraries( searchtest ${KDE4_KDECORE_LIBS} ${QT_QTGUI_LIBRARY} ${QT_QTTEST_LIBRARY} okularcore ) +kde4_add_unit_test( annotationstest annotationstest.cpp ) +target_link_libraries( annotationstest ${KDE4_KDECORE_LIBS} ${QT_QTGUI_LIBRARY} ${QT_QTTEST_LIBRARY} okularcore ) + kde4_add_unit_test( urldetecttest urldetecttest.cpp ) target_link_libraries( urldetecttest ${KDE4_KDECORE_LIBS} ${QT_QTTEST_LIBRARY} )