15.1.149. tablet_qt/graphics/geometry.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS 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 3 of the License, or
    (at your option) any later version.

    CamCOPS 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 CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/


#include "geometry.h"
#include <QtMath>
#include "maths/mathfunc.h"
using mathfunc::mod;


namespace geometry
{


const qreal DEG_0 = 0.0;
const qreal DEG_90 = 90.0;
const qreal DEG_270 = 270.0;
const qreal DEG_180 = 180.0;
const qreal DEG_360 = 360.0;


int sixteenthsOfADegree(const qreal degrees)
{
    // https://doc.qt.io/qt-6.5/qpainter.html#drawPie
    return qRound(degrees * 16.0);
}


qreal normalizeHeading(const qreal heading_deg)
{
    return mod(heading_deg, DEG_360);
}


bool headingNearlyEq(const qreal heading_deg, const qreal value_deg)
{
    return qFuzzyIsNull(normalizeHeading(heading_deg - value_deg));
}


bool headingInRange(qreal first_bound_deg,
                    qreal heading_deg,
                    qreal second_bound_deg,
                    const bool inclusive)
{
    // The values in degrees are taken as a COMPASS HEADING, i.e. increasing
    // is clockwise. The valid sector is defined CLOCKWISE from the first bound
    // to the second.
    first_bound_deg = normalizeHeading(first_bound_deg);
    heading_deg = normalizeHeading(heading_deg);
    second_bound_deg = normalizeHeading(second_bound_deg);
    // First, we deal with "on the boundary" conditions:
    if (heading_deg == first_bound_deg || heading_deg == second_bound_deg) {
        return inclusive;
    }
    const bool range_increases = first_bound_deg < second_bound_deg;
    qreal lower_bound;
    qreal upper_bound;
    if (range_increases) {
        lower_bound = first_bound_deg;
        upper_bound = second_bound_deg;
    } else {
        lower_bound = second_bound_deg;
        upper_bound = first_bound_deg;
    }
    const bool within = lower_bound < heading_deg && heading_deg < upper_bound;
    // Second bound is clockwise ("right") from first.
    // If the second bound is numerically greater than the first, then
    // we have a simple range that doesn't cross "North" (0 = 360),
    // and the heading is in range if it's within the two. For example,
    // if the range is (50, 70), then the heading is in range if
    // 50 < x < 70. However, if the range decreases, we're crossing North,
    // e.g. (350, 10); in that case, the heading is in range if and only if
    // it is NOT true that 10 < x < 350.
    return within == range_increases;
}


qreal convertHeadingFromTrueNorth(const qreal true_north_heading_deg,
                                  const qreal pseudo_north_deg,
                                  const bool normalize)
{
    // Example: pseudo_north_deg is 30;
    // then 0 in true North is -30 in pseudo-North.
    const qreal h = true_north_heading_deg - pseudo_north_deg;
    return normalize ? normalizeHeading(h) : h;
}


qreal convertHeadingToTrueNorth(const qreal pseudo_north_heading_deg,
                                const qreal pseudo_north_deg,
                                const bool normalize)
{
    // Inverts convertHeadingFromTrueNorth().
    const qreal h = pseudo_north_heading_deg + pseudo_north_deg;
    return normalize ? normalizeHeading(h) : h;
}


QPointF polarToCartesian(const qreal r, const qreal theta_deg)
{
    // theta == 0 implies along the x axis in a positive direction (right).
    const qreal theta_rad = qDegreesToRadians(theta_deg);
    return QPointF(r * qCos(theta_rad), r * qSin(theta_rad));
}


qreal distanceBetween(const QPointF& from, const QPointF& to)
{
    const qreal dx = to.x() - from.x();
    const qreal dy = to.y() - from.y();
    // Pythagoras:
    return qSqrt(qPow(dx, 2) + qPow(dy, 2));
}


qreal polarThetaToHeading(const qreal theta_deg, const qreal north_deg)
{
    // Polar coordinates have theta 0 == East, and theta positive is
    // clockwise (in Qt coordinates with y down).
    // Compass headings have 0 == North, unless adjusted by
    // north_deg (e.g. specifying north_deg = 90 makes the heading 0 when
    // actually East), and positive clockwise.
    // - The first step converts to "clockwise, up is 0":
    const qreal true_north_heading = theta_deg + DEG_90;
    return convertHeadingFromTrueNorth(true_north_heading, north_deg);
}


qreal headingToPolarThetaDeg(const qreal heading_deg,
                          const qreal north_deg,
                          const bool normalize)
{
    // Polar coordinates have theta 0 == East, and theta positive is
    // anticlockwise. Compass headings have 0 == North, unless adjusted by
    // north_deg (e.g. specifying north_deg = 90 makes the heading 0 when
    // actually East), and positive clockwise.
    const qreal true_north_heading = convertHeadingToTrueNorth(
                heading_deg, north_deg, normalize);
    const qreal theta = true_north_heading - DEG_90;
    return normalize ? normalizeHeading(theta) : theta;
}


qreal polarThetaDeg(const QPointF& from, const QPointF& to)
{
    const qreal dx = to.x() - from.x();
    const qreal dy = to.y() - from.y();
    if (qFuzzyIsNull(dx) && qFuzzyIsNull(dy)) {
        // Nonsensical; no movement.
        return 0.0;
    }
    // The arctan function will give us 0 = East, the geometric form.
    return qRadiansToDegrees(qAtan2(dy, dx));
}


qreal polarThetaDeg(const QPointF& to)
{
    return polarThetaDeg(QPointF(0, 0), to);
}


qreal headingDegrees(const QPointF& from, const QPointF& to, qreal north_deg)
{
    // Returns a COMPASS HEADING (0 is North = up).
    return polarThetaToHeading(polarThetaDeg(from, to), north_deg);
}


bool lineSegmentsIntersect(const QPointF& first_from, const QPointF& first_to,
                           const QPointF& second_from, const QPointF& second_to)
{
    const LineSegment s1(first_from, first_to);
    const LineSegment s2(second_from, second_to);
    return s1.intersects(s2);
}


bool pointOnLineSegment(const QPointF& point,
                        const QPointF& line_start, const QPointF& line_end)
{
    const LineSegment ls(line_start, line_end);
    return ls.pointOn(point);
}


LineSegment lineFromPointInHeadingWithRadius(const QPointF& point,
                                             const qreal heading_deg,
                                             const qreal north_deg,
                                             const qreal radius)
{
    const qreal theta = headingToPolarThetaDeg(heading_deg, north_deg);
    const QPointF distant_point = point + polarToCartesian(radius, theta);
    return LineSegment(point, distant_point);
}


bool lineCrossesHeadingWithinRadius(
        const QPointF& from, const QPointF& to,
        const QPointF& point, const qreal heading_deg,
        const qreal north_deg, const qreal radius)
{
    if (from == to) {
        return false;
    }
    const LineSegment ls_trajectory = lineFromPointInHeadingWithRadius(
                point, heading_deg, radius, north_deg);
    const LineSegment from_to = LineSegment(from, to);
    return from_to.intersects(ls_trajectory);
}


bool linePassesBelowPoint(const QPointF& from, const QPointF& to,
                          const QPointF& point)
{
    return lineCrossesHeadingWithinRadius(from, to, point, DEG_180, 0,
                                          QWIDGETSIZE_MAX);
}


}  // namespace geometry