Ticket #17119: clip-v2.patch

File clip-v2.patch, 14.6 KB (added by GerdP, 5 years ago)

clip only when shape is partially outside view, some code improvements

  • src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java

     
    8787import org.openstreetmap.josm.tools.ImageProvider;
    8888import org.openstreetmap.josm.tools.JosmRuntimeException;
    8989import org.openstreetmap.josm.tools.Logging;
     90import org.openstreetmap.josm.tools.ShapeClipper;
    9091import org.openstreetmap.josm.tools.Utils;
    9192import org.openstreetmap.josm.tools.bugreport.BugReport;
    9293
     
    485486            Shape clip = shape;
    486487            if (pfClip != null) {
    487488                clip = pfClip.createTransformedShape(mapState.getAffineTransform());
     489            } else if (!mapState.isInsideViewClipRectangle(shape)) {
     490                // clip with fast algorithm first
     491                Shape clip2 = ShapeClipper.clipShape(shape, mapState.getViewClipRectangle().getInView());
     492                if (clip2 != null) {
     493                    clip = clip2;
     494                }
    488495            }
    489496            g.clip(clip);
    490497            g.setStroke(new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, mitterLimit));
    491             g.draw(shape);
     498            if (pfClip == null)
     499                g.draw(shape);
     500            else
     501                g.draw(clip);
    492502            g.setClip(oldClip);
    493503            g.setStroke(new BasicStroke());
    494504        }
  • src/org/openstreetmap/josm/gui/MapViewState.java

     
    33
    44import java.awt.Container;
    55import java.awt.Point;
     6import java.awt.Shape;
    67import java.awt.geom.AffineTransform;
    78import java.awt.geom.Area;
    89import java.awt.geom.Path2D;
     
    304305    }
    305306
    306307    /**
     308     * Gets a rectangle that is several pixel bigger than the view in EastNorth.
     309     * @return The rectangle.
     310     */
     311    public Rectangle2D getEastNorthClipRectangle() {
     312        return new Rectangle2D.Double(topLeft.east() - CLIP_BOUNDS, topLeft.north() - (viewHeight + CLIP_BOUNDS),
     313                viewWidth + 2 * CLIP_BOUNDS, viewHeight + 2 * CLIP_BOUNDS);
     314    }
     315
     316    /**
     317     * Test if the area is inside the view clip rectangle (it may touch it).
     318     * @param area The area, interpreted in east/north space.
     319     * @return true if it is inside or touches it
     320     */
     321    public boolean isInsideViewClipRectangle(Shape area) {
     322        Rectangle2D bounds = area.getBounds2D();
     323        if (bounds.isEmpty()) return false;
     324        return (getEastNorthClipRectangle().contains(bounds));
     325
     326    }
     327
     328    /**
    307329     * Returns the area for the given bounds.
    308330     * @param bounds bounds
    309331     * @return the area for the given bounds
  • src/org/openstreetmap/josm/gui/draw/MapViewPath.java

     
    2424 * @author Michael Zangl
    2525 * @since 10875
    2626 */
    27 public class MapViewPath extends MapPath2D {
     27public class MapViewPath extends MapPath2D implements Shape {
    2828
    2929    private final MapViewState state;
    3030
  • src/org/openstreetmap/josm/tools/ShapeClipper.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.tools;
     3
     4import java.awt.Shape;
     5import java.awt.geom.Path2D;
     6import java.awt.geom.PathIterator;
     7import java.awt.geom.Rectangle2D;
     8import java.util.Arrays;
     9
     10/**
     11 * Tools to clip a shape based on the Sutherland-Hodgman algorithm.
     12 * See https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm
     13 * @author Gerd Petermann
     14 *
     15 */
     16public final class ShapeClipper {
     17    private static final int LEFT = 0;
     18    private static final int TOP = 1;
     19    private static final int RIGHT = 2;
     20    private static final int BOTTOM = 3;
     21
     22    private ShapeClipper() {
     23        // Hide default constructor for utils classes
     24    }
     25
     26    /**
     27     * Clip a given (closed) shape with a given rectangle.
     28     * @param shape the subject shape to clip
     29     * @param clippingRect the clipping rectangle
     30     * @return the intersection of the shape and the rectangle
     31     * or null if they don't intersect or the shape is not closed.
     32     * The intersection may contain dangling edges.
     33     */
     34    public static Path2D.Double clipShape(Shape shape, Rectangle2D clippingRect) {
     35        double minX = Double.POSITIVE_INFINITY, minY = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY,
     36                maxY = Double.NEGATIVE_INFINITY;
     37        PathIterator pit = shape.getPathIterator(null);
     38        double[] points = new double[512];
     39        int num = 0;
     40        Path2D.Double result = null;
     41        double[] res = new double[6];
     42        while (!pit.isDone()) {
     43            int type = pit.currentSegment(res);
     44            double x = res[0];
     45            double y = res[1];
     46            if (x < minX)
     47                minX = x;
     48            if (x > maxX)
     49                maxX = x;
     50            if (y < minY)
     51                minY = y;
     52            if (y > maxY)
     53                maxY = y;
     54            switch (type) {
     55            case PathIterator.SEG_LINETO:
     56            case PathIterator.SEG_MOVETO:
     57                if (num + 2 >= points.length) {
     58                    points = Arrays.copyOf(points, points.length * 2);
     59                }
     60                points[num++] = x;
     61                points[num++] = y;
     62                break;
     63            case PathIterator.SEG_CLOSE:
     64                Path2D.Double segment = null;
     65                if (!clippingRect.contains(minX, minY) || !clippingRect.contains(maxX, maxY)) {
     66                    Rectangle2D.Double bbox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
     67                    segment = clipSinglePathWithSutherlandHodgman(points, num, clippingRect, bbox);
     68                } else
     69                    segment = pointsToPath2D(points, num);
     70                if (segment != null) {
     71                    if (result == null)
     72                        result = segment;
     73                    else
     74                        result.append(segment, false);
     75                }
     76                num = 0;
     77                minX = minY = Double.POSITIVE_INFINITY;
     78                maxX = maxY = Double.NEGATIVE_INFINITY;
     79                break;
     80            default:
     81                Logging.error("Unsupported path iterator type " + type + ". This is not supported by " + ShapeClipper.class.getName() + ".");
     82            }
     83            pit.next();
     84        }
     85        if (num >= 2) {
     86            // last type was not a SEG_CLOSE
     87            if (points[0] == points[num - 2] && points[1] == points[num - 1]) {
     88                // not closed with closePath() but first and last point are equal
     89                Path2D.Double segment = null;
     90                if (!clippingRect.contains(minX, minY) || !clippingRect.contains(maxX, maxY)) {
     91                    Rectangle2D.Double bbox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
     92                    segment = clipSinglePathWithSutherlandHodgman(points, num, clippingRect, bbox);
     93                } else
     94                    segment = pointsToPath2D(points, num);
     95                if (segment != null) {
     96                    if (result == null)
     97                        result = segment;
     98                    else
     99                        result.append(segment, false);
     100                }
     101            } else {
     102                Logging.warn(ShapeClipper.class.getName() + " shape is not closed.");
     103                return null;
     104            }
     105        }
     106        return result;
     107    }
     108
     109    /**
     110     * Convert a list of points to a Path2D.Double
     111     * @param points the pairs
     112     * @param num the number of valid values in points
     113     * @return the path or null if the path describes a point or line.
     114     */
     115    private static Path2D.Double pointsToPath2D(double[] points, int num) {
     116        if (num < 2)
     117            return null;
     118        if (points[0] == points[num - 2] && points[1] == points[num - 1])
     119            num -= 2;
     120        if (num < 6)
     121            return null;
     122        Path2D.Double path = new Path2D.Double(Path2D.WIND_NON_ZERO, num / 2 + 2);
     123        double lastX = points[0], lastY = points[1];
     124        path.moveTo(lastX, lastY);
     125        int numOut = 1;
     126        for (int i = 2; i < num;) {
     127            double x = points[i++], y = points[i++];
     128            if (x != lastX || y != lastY) {
     129                path.lineTo(x, y);
     130                lastX = x;
     131                lastY = y;
     132                ++numOut;
     133            }
     134        }
     135        if (numOut < 3)
     136            return null;
     137        path.closePath();
     138        return path;
     139    }
     140
     141    /**
     142     * Clip a single path with a given rectangle using the Sutherland-Hodgman algorithm. This is much faster compared to
     143     * the area.intersect method, but may create dangling edges.
     144     * @param points a list of longitude+latitude pairs
     145     * @param num the number of valid values in points
     146     * @param clippingRect the clipping rectangle
     147     * @param bbox the bounding box of the path
     148     * @return the clipped path as a Path2D.Double or null if the result is empty
     149     */
     150    private static Path2D.Double clipSinglePathWithSutherlandHodgman(double[] points, int num, Rectangle2D clippingRect,
     151            Rectangle2D.Double bbox) {
     152        if (num <= 2 || !bbox.intersects(clippingRect)) {
     153            return null;
     154        }
     155
     156        int countVals = num;
     157        if (points[0] == points[num - 2] && points[1] == points[num - 1]) {
     158            countVals -= 2;
     159        }
     160        double[] outputList = points;
     161        double[] input;
     162
     163        double leftX = clippingRect.getMinX();
     164        double rightX = clippingRect.getMaxX();
     165        double lowerY = clippingRect.getMinY();
     166        double upperY = clippingRect.getMaxY();
     167        boolean eIsIn = false, sIsIn = false;
     168        for (int side = LEFT; side <= BOTTOM; side++) {
     169            if (countVals < 6)
     170                return null; // ignore point or line
     171
     172            boolean skipTestForThisSide;
     173            switch (side) {
     174            case LEFT:
     175                skipTestForThisSide = (bbox.getMinX() >= leftX);
     176                break;
     177            case TOP:
     178                skipTestForThisSide = (bbox.getMaxY() < upperY);
     179                break;
     180            case RIGHT:
     181                skipTestForThisSide = (bbox.getMaxX() < rightX);
     182                break;
     183            default:
     184                skipTestForThisSide = (bbox.getMinY() >= lowerY);
     185            }
     186            if (skipTestForThisSide)
     187                continue;
     188
     189            input = outputList;
     190            outputList = new double[countVals + 16];
     191            double sLon = 0, sLat = 0;
     192            double pLon = 0, pLat = 0; // intersection
     193            int posIn = countVals - 2;
     194            int posOut = 0;
     195            for (int i = 0; i < countVals + 2; i += 2) {
     196                if (posIn >= countVals)
     197                    posIn = 0;
     198                double eLon = input[posIn++];
     199                double eLat = input[posIn++];
     200                switch (side) {
     201                case LEFT:
     202                    eIsIn = (eLon >= leftX);
     203                    break;
     204                case TOP:
     205                    eIsIn = (eLat < upperY);
     206                    break;
     207                case RIGHT:
     208                    eIsIn = (eLon < rightX);
     209                    break;
     210                default:
     211                    eIsIn = (eLat >= lowerY);
     212                }
     213                if (i > 0) {
     214                    if (eIsIn != sIsIn) {
     215                        // compute intersection
     216                        double slope;
     217                        if (eLon != sLon)
     218                            slope = (eLat - sLat) / (eLon - sLon);
     219                        else
     220                            slope = 1;
     221
     222                        switch (side) {
     223                        case LEFT:
     224                            pLon = leftX;
     225                            pLat = slope * (leftX - sLon) + sLat;
     226                            break;
     227                        case RIGHT:
     228                            pLon = rightX;
     229                            pLat = slope * (rightX - sLon) + sLat;
     230                            break;
     231
     232                        case TOP:
     233                            if (eLon != sLon)
     234                                pLon = sLon + (upperY - sLat) / slope;
     235                            else
     236                                pLon = sLon;
     237                            pLat = upperY;
     238                            break;
     239                        default: // BOTTOM
     240                            if (eLon != sLon)
     241                                pLon = sLon + (lowerY - sLat) / slope;
     242                            else
     243                                pLon = sLon;
     244                            pLat = lowerY;
     245                            break;
     246
     247                        }
     248                    }
     249                    int toAdd = 0;
     250                    if (eIsIn) {
     251                        if (!sIsIn) {
     252                            toAdd += 2;
     253                        }
     254                        toAdd += 2;
     255                    } else {
     256                        if (sIsIn) {
     257                            toAdd += 2;
     258                        }
     259                    }
     260                    if (posOut + toAdd >= outputList.length) {
     261                        // unlikely
     262                        outputList = Arrays.copyOf(outputList, outputList.length * 2);
     263                    }
     264                    if (eIsIn) {
     265                        if (!sIsIn) {
     266                            outputList[posOut++] = pLon;
     267                            outputList[posOut++] = pLat;
     268                        }
     269                        outputList[posOut++] = eLon;
     270                        outputList[posOut++] = eLat;
     271                    } else {
     272                        if (sIsIn) {
     273                            outputList[posOut++] = pLon;
     274                            outputList[posOut++] = pLat;
     275                        }
     276                    }
     277                }
     278                // S = E
     279                sLon = eLon;
     280                sLat = eLat;
     281                sIsIn = eIsIn;
     282            }
     283            countVals = posOut;
     284        }
     285        return pointsToPath2D(outputList, countVals);
     286    }
     287}