chromium/android_webview/java/src/org/chromium/android_webview/gfx/RectUtils.java

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview.gfx;

import android.graphics.Rect;

import androidx.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;

/** Utility functions for calculating Rectangle properties (i.e. Area of a single Rect) */
public final class RectUtils {
    private RectUtils() {}

    public static int getRectArea(Rect rect) {
        return rect.width() * rect.height();
    }

    /** Segment Type Constants */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({SegmentType.START, SegmentType.END})
    private @interface SegmentType {
        int START = 0;
        int END = 1;
    }

    private static int compareSegmentTypes(int s1, int s2) {
        if (s1 == s2) {
            return 0;
        } else if (s1 == SegmentType.START && s2 == SegmentType.END) {
            return -1;
        } else {
            return 1;
        }
    }

    private static class HorizontalSegment implements Comparable<HorizontalSegment> {
        public int mX;
        public int mTop;
        public int mBottom;
        public @SegmentType int mSegmentType;

        public HorizontalSegment() {
            set(0, 0, 0, SegmentType.START);
        }

        public void set(int x, int top, int bottom, @SegmentType int segmentType) {
            this.mX = x;
            this.mTop = top;
            this.mBottom = bottom;
            this.mSegmentType = segmentType;
        }

        @Override
        public int compareTo(HorizontalSegment other) {
            if (mX == other.mX) {
                return compareSegmentTypes(mSegmentType, other.mSegmentType);
            }
            return mX - other.mX;
        }
    }

    private static class VerticalSegment implements Comparable<VerticalSegment> {
        public int mY;
        public @SegmentType int mSegmentType;

        public VerticalSegment() {
            set(0, SegmentType.START);
        }

        public void set(int y, @SegmentType int segmentType) {
            this.mY = y;
            this.mSegmentType = segmentType;
        }

        public void set(VerticalSegment other) {
            set(other.mY, other.mSegmentType);
        }

        @Override
        public int compareTo(VerticalSegment other) {
            if (mY == other.mY) {
                return compareSegmentTypes(mSegmentType, other.mSegmentType);
            }
            return mY - other.mY;
        }
    }

    private static void insertSorted(
            VerticalSegment arr[], int n, VerticalSegment verticalSegment, int capacity) {
        assert n < capacity;

        int i;
        for (i = n - 1; (i >= 0 && arr[i].compareTo(verticalSegment) > 0); i--) {
            arr[i + 1].set(arr[i]);
        }

        int insert_index = i + 1;
        assert insert_index >= 0 && insert_index < capacity;
        arr[insert_index].set(verticalSegment);
    }

    private static int deleteElement(
            VerticalSegment arr[], int n, VerticalSegment verticalSegment) {
        int pos = Arrays.binarySearch(arr, 0, n, verticalSegment);
        if (pos < 0) {
            return -1;
        }

        // In the case of duplicate values, either one can be removed
        for (int i = pos + 1; i < n; i++) {
            arr[i - 1].set(arr[i]);
        }
        return n - 1;
    }

    private static int getCoverageOfVerticalSegments(
            VerticalSegment vSegments[], int numVerticalSegments) {
        int scanCount = 0;
        int coveredPixels = 0;
        int start = -1;

        for (int i = 0; i < numVerticalSegments; i++) {
            VerticalSegment verticalSegment = vSegments[i];
            if (scanCount == 0 && verticalSegment.mSegmentType == SegmentType.START) {
                start = verticalSegment.mY;
            } else if (scanCount == 1 && verticalSegment.mSegmentType == SegmentType.END) {
                coveredPixels += verticalSegment.mY - start;
            }
            scanCount += verticalSegment.mSegmentType == SegmentType.START ? 1 : -1;
        }
        return coveredPixels;
    }

    private static HorizontalSegment sHorizontalSegments[];
    private static VerticalSegment sVerticalSegments[];
    private static VerticalSegment sVerticalSegment1 = new VerticalSegment();
    private static VerticalSegment sVerticalSegment2 = new VerticalSegment();
    private static Rect sClippedRects[];

    /*
            This is a 2d extension of the 1d range intersection problem.
            In one dimension we are interested in calculating the
            intersected set of ranges for an input. To do this we decompose
            each input range into a start and an end position, plus whether
            it is entering a range or leaving it. Once these decomposed
            positions are sorted, we can compute the intersection by
            iterating over the list and recording transitions from not being
            in a range to being in a range, and vice versa.

            E.g. [1,4] U [2,5] U [7,9] -> [1,+1] [2,+1] [4,-1] [5,-1]
            [7,+1] [9,-1]. Then, summing the second component as we
            traverse, and looking for 0->1 and 1->0 transitions, we end up
            finding the union ranges [1,5], [7,9]

            In order to extend this to 2d axis aligned rectangles, we
            decompose rectangles into top and bottom edges that add or
            remove a range from the 1d data data structure. Before we add or
            remove a range to the 1d data structure we accumulate area equal
            to the current 1d coverage multiplied by the delta-y from the
            last point at which we updated the coverage.

    1  4  7   11 14  18
    1     +------+         [4,+1], [11,-1] cov=7 area += 0
    2  +----------------+  [1,+1], [4,+1], [11,-1], [18,-1], cov=17, rea += 7*1
    3  |  |  +------+   |  [1,+1], [4,+1], [7,+1], [11,-1], [14,-1], [18,-1], cov=17 area += 17*1
    4  |  +------+  |   |  [1,+1], [7,+1], [14,-1], [18,-1], cov=17 area += 17*1
    5  +----------------+  [7,+1], [14,-1] cov=7 area += 17*1
    6        +------+      [] area += 7*1
        */

    public static int calculatePixelsOfCoverage(Rect screenRect, List<Rect> coverageRects) {
        if (coverageRects.size() == 0) {
            return 0;
        }

        // Always allocate enough space for all passed rects and never trim allocations as a result
        // of clipping
        if ((sClippedRects == null ? 0 : sClippedRects.length) < coverageRects.size()) {
            sClippedRects = new Rect[coverageRects.size()];
        }

        int numClippedRects = 0;
        for (int i = 0; i < coverageRects.size(); i++) {
            Rect clipRect = coverageRects.get(i);
            if (clipRect.intersect(screenRect)) { // This line may modify the value of the passed
                // in coverage rects
                sClippedRects[numClippedRects++] = clipRect;
            }
        }

        if (numClippedRects == 0) {
            return 0;
        }

        int maxSegments = numClippedRects * 2;
        int numVerticalSegments = 0;

        if ((sHorizontalSegments == null ? 0 : sHorizontalSegments.length) < maxSegments) {
            sHorizontalSegments = new HorizontalSegment[maxSegments];
            sVerticalSegments = new VerticalSegment[maxSegments];

            for (int i = 0; i < maxSegments; i++) {
                sHorizontalSegments[i] = new HorizontalSegment();
                sVerticalSegments[i] = new VerticalSegment();
            }
        }

        for (int i = 0; i < maxSegments; i += 2) {
            Rect coverageRect = sClippedRects[i / 2];
            sHorizontalSegments[i].set(
                    coverageRect.left, coverageRect.top, coverageRect.bottom, SegmentType.START);
            sHorizontalSegments[i + 1].set(
                    coverageRect.right, coverageRect.top, coverageRect.bottom, SegmentType.END);
        }

        Arrays.sort(sHorizontalSegments, 0, maxSegments);

        int prev_x = -1;
        int coveredPixels = 0;
        for (int i = 0; i < maxSegments; i++) {
            HorizontalSegment hSegment = sHorizontalSegments[i];
            coveredPixels +=
                    getCoverageOfVerticalSegments(sVerticalSegments, numVerticalSegments)
                            * (hSegment.mX - prev_x);
            sVerticalSegment1.set(hSegment.mTop, SegmentType.START);
            sVerticalSegment2.set(hSegment.mBottom, SegmentType.END);

            if (hSegment.mSegmentType == SegmentType.START) {
                insertSorted(
                        sVerticalSegments, numVerticalSegments, sVerticalSegment1, maxSegments);
                numVerticalSegments++;
                insertSorted(
                        sVerticalSegments, numVerticalSegments, sVerticalSegment2, maxSegments);
                numVerticalSegments++;
            } else {
                int ret;
                ret = deleteElement(sVerticalSegments, numVerticalSegments, sVerticalSegment1);
                assert ret != -1;
                numVerticalSegments = ret;
                ret = deleteElement(sVerticalSegments, numVerticalSegments, sVerticalSegment2);
                assert ret != -1;
                numVerticalSegments = ret;
            }
            prev_x = hSegment.mX;
        }

        return coveredPixels;
    }
}