Fix usability issues with selecting annotations

REVIEW: 109627
This commit is contained in:
Peter Grasch 2013-06-23 19:19:25 +02:00 committed by Albert Astals Cid
parent e159522131
commit 4d4dd68ca2
7 changed files with 315 additions and 9 deletions

View file

@ -14,6 +14,9 @@
#include <QtGui/QApplication>
#include <QtGui/QColor>
// DBL_MAX
#include <float.h>
// 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<NormalizedPoint>& path )
{
double distance = DBL_MAX;
double thisDistance;
QLinkedList<NormalizedPoint>::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<NormalizedPoint> 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<NormalizedPoint> 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<NormalizedPoint>& 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 );

View file

@ -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

View file

@ -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;

View file

@ -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<Annotation*>(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 );
}
}
}

View file

@ -15,6 +15,7 @@
#include <QtGui/QPainterPath>
#include <QtGui/QTransform>
#include <kdebug.h>
#include <math.h>
#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.
*/

View file

@ -49,6 +49,8 @@
using namespace Okular;
static const double distanceConsideredEqual = 25; // 5px
static void deleteObjectRects( QLinkedList< ObjectRect * >& rects, const QSet<ObjectRect::ObjectType>& 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;

View file

@ -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} )