15.1.151. 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