BoundingBoxRounded.java

package org.opentrafficsim.base.geometry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.djutils.draw.line.Polygon2d;
import org.djutils.draw.point.Point2d;
import org.djutils.exceptions.Throw;

/**
 * Bounds defined by a rounded rectangle.
 * <p>
 * Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
 * </p>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 */
public class BoundingBoxRounded implements OtsBounds2d
{

    /** Number of line segments in polygon representation for the curves if they are 4 full quarter circles. */
    private final static int POLYGON_STEPS = 128;

    /** Half length along y dimension. */
    private final double dx;

    /** Half length along y dimension. */
    private final double dy;

    /** Polygon representation. */
    private Polygon2d polygon;

    /** Rounding radius. */
    private final double r;

    /** Max x coordinate, can be lower than dx due to large r. */
    private final double maxX;

    /** Max y coordinate, can be lower than dy due to large r. */
    private final double maxY;

    /**
     * Constructor.
     * @param dx double; complete length along x dimension.
     * @param dy double; complete length along y dimension.
     * @param r double; radius of rounding, must be positive.
     */
    public BoundingBoxRounded(final double dx, final double dy, final double r)
    {
        /*-
         * Equation derived from r^2 = (r-dx)^2 + (r-dy^2)  [note: dx and dy here as half of input values, i.e. this.dx/this.dy]
         * 
         *                dx
         *    ___       ______
         * ^ |   ''--_ |      | dy
         * | |---------o------
         * | | r-dx  / |'.
         * r |      /  |  '.
         * | |   r/    |    \
         * | |   /     |     \
         * | | /   r-dy|      |
         * v |/________|______|
         *    <-------r------>
         */
        this.dx = Math.abs(dx) / 2.0;
        this.dy = Math.abs(dy) / 2.0;
        Throw.when(r >= this.dx + this.dy + Math.sqrt(2.0 * this.dx * this.dy), IllegalArgumentException.class,
                "Radius makes rounded rectangle non-existent.");
        Throw.when(r < 0.0, IllegalArgumentException.class, "Radius must be positive.");
        this.r = r;
        this.maxX = this.dx - signedDistance(new Point2d(this.dx, 0.0));
        this.maxY = this.dy - signedDistance(new Point2d(0.0, this.dy));
    }

    /** {@inheritDoc} */
    @Override
    public double getMinX()
    {
        return -this.maxX;
    }

    /** {@inheritDoc} */
    @Override
    public double getMaxX()
    {
        return this.maxX;
    }

    /** {@inheritDoc} */
    @Override
    public double getMinY()
    {
        return -this.maxY;
    }

    /** {@inheritDoc} */
    @Override
    public double getMaxY()
    {
        return this.maxY;
    }

    /** {@inheritDoc} */
    @Override
    public boolean contains(final Point2d point) throws NullPointerException
    {
        return signedDistance(point) < 0.0;
    }

    /** {@inheritDoc} */
    @Override
    public boolean covers(final Point2d point) throws NullPointerException
    {
        return signedDistance(point) <= 0.0;
    }

    /**
     * {@inheritDoc}
     * @see <a href="https://iquilezles.org/articles/distfunctions/">Signed distance functions by Inigo Quilez</a>
     */
    @Override
    public double signedDistance(final Point2d point)
    {
        double qx = Math.abs(point.x) - this.dx + this.r;
        double qy = Math.abs(point.y) - this.dy + this.r;
        return Math.hypot(Math.max(qx, 0.0), Math.max(qy, 0.0)) + Math.min(Math.max(qx, qy), 0.0) - this.r;
    }

    /** {@inheritDoc} */
    @Override
    public Polygon2d asPolygon()
    {
        if (this.polygon == null)
        {
            // calculate for top right quadrant only, others are negative or reversed copies
            int n = POLYGON_STEPS / 4;
            List<Point2d> pq = new ArrayList<>();
            for (int i = 0; i <= n; i++)
            {
                double ang = (0.5 * Math.PI * i) / n;
                double x = this.dx + Math.cos(ang) * this.r - this.r;
                double y = this.dy + Math.sin(ang) * this.r - this.r;
                if (x >= 0.0 && y >= 0.0) // else, radius larger than at least one of this.dx and this.dy, i.e. no full quarters
                {
                    pq.add(new Point2d(x, y));
                }
            }

            List<Point2d> pqReversed = new ArrayList<>(pq);
            Collections.reverse(pqReversed);
            List<Point2d> points = new ArrayList<>(pq); // top right quadrant (y = up)
            pqReversed.forEach((p) -> points.add(new Point2d(-p.x, p.y))); // top left quadrant
            pq.forEach((p) -> points.add(p.neg())); // bottom left quadrant
            pqReversed.forEach((p) -> points.add(new Point2d(p.x, -p.y))); // bottom right quadrant
            this.polygon = new Polygon2d(true, points);
        }
        return this.polygon;
    }

}