private void addCollinear(final boolean addStartPoint) { /** * This test could probably be done more efficiently, but the situation of exact collinearity * should be fairly rare. */ m_lineIntersector.computeIntersection( m_segment0.p0, m_segment0.p1, m_segment1.p0, m_segment1.p1); final int numInt = m_lineIntersector.getIntersectionNum(); /** * if numInt is < 2, the lines are parallel and in the same direction. In this case the point * can be ignored, since the offset lines will also be parallel. */ if (numInt >= 2) { /** * segments are collinear but reversing. Add an "end-cap" fillet all the way around to other * direction This case should ONLY happen for LineStrings, so the orientation is always CW. * (Polygons can never have two consecutive segments which are parallel but reversed, because * that would be a self intersection. */ if (m_bufferParams.getJoinStyle() == BufferParameters.JOIN_BEVEL || m_bufferParams.getJoinStyle() == BufferParameters.JOIN_MITRE) { if (addStartPoint) m_bufferBuilder.addVertices(m_buffer0.p1); m_bufferBuilder.addVertices(m_buffer1.p0); } else { addFillet(m_segment0.p1, m_buffer0.p1, m_buffer1.p0, CGAlgorithms.CLOCKWISE, m_distance1); } } }
/** * Adds a mitre join connecting the two reflex offset segments. The mitre will be beveled if it * exceeds the mitre ratio limit. * * @param offset0 the first offset segment * @param offset1 the second offset segment * @param distance the offset distance */ private void addMitreJoin( final Coordinate p, final LineSegment offset0, final LineSegment offset1, final double distance) { boolean isMitreWithinLimit = true; Coordinate intPt = null; /** * This computation is unstable if the offset segments are nearly collinear. Howver, this * situation should have been eliminated earlier by the check for whether the offset segment * endpoints are almost coincident */ try { intPt = HCoordinate.intersection(offset0.p0, offset0.p1, offset1.p0, offset1.p1); final double mitreRatio = distance <= 0.0 ? 1.0 : intPt.distance(p) / Math.abs(distance); if (mitreRatio > m_bufferParams.getMitreLimit()) isMitreWithinLimit = false; } catch (final NotRepresentableException ex) { intPt = new Coordinate(0, 0); isMitreWithinLimit = false; } if (isMitreWithinLimit) { m_bufferBuilder.addVertices(intPt); } else { addLimitedMitreJoin(distance, m_bufferParams.getMitreLimit()); } }
/** * Adds points for a circular fillet arc between two specified angles. The start and end point for * the fillet are not added - the caller must add them if required. * * @param direction is -1 for a CW angle, 1 for a CCW angle * @param radius the radius of the fillet */ private void addFillet( final Coordinate p, final double startAngle, final double endAngle, final int direction, final double radius) { final int directionFactor = direction == CGAlgorithms.CLOCKWISE ? -1 : 1; final double totalAngle = Math.abs(startAngle - endAngle); final int nSegs = (int) (totalAngle / m_filletAngleQuantum + 0.5); if (nSegs < 1) return; // no segments because angle is less than increment - nothing to do! double initAngle, currAngleInc; // choose angle increment so that each segment has equal length initAngle = 0.0; currAngleInc = totalAngle / nSegs; double currAngle = initAngle; final Coordinate pt = new Coordinate(); while (currAngle < totalAngle) { final double angle = startAngle + directionFactor * currAngle; pt.x = p.x + radius * Math.cos(angle); pt.y = p.y + radius * Math.sin(angle); m_bufferBuilder.addVertices(pt); currAngle += currAngleInc; } }
/** * Adds a limited mitre join connecting the two reflex offset segments. A limited mitre is a mitre * which is beveled at the distance determined by the mitre ratio limit. * * @param offset0 the first offset segment * @param offset1 the second offset segment * @param distance the offset distance * @param mitreLimit the mitre limit ratio */ private void addLimitedMitreJoin(final double distance, final double mitreLimit) { final Coordinate basePt = m_segment0.p1; final double ang0 = Angle.angle(basePt, m_segment0.p0); // oriented angle between segments final double angDiff = Angle.angleBetweenOriented(m_segment0.p0, basePt, m_segment1.p1); // half of the interior angle final double angDiffHalf = angDiff / 2; // angle for bisector of the interior angle between the segments final double midAng = Angle.normalize(ang0 + angDiffHalf); // rotating this by PI gives the bisector of the reflex angle final double mitreMidAng = Angle.normalize(midAng + Math.PI); // the miterLimit determines the distance to the mitre bevel final double mitreDist = mitreLimit * distance; // the bevel delta is the difference between the buffer distance // and half of the length of the bevel segment final double bevelDelta = mitreDist * Math.abs(Math.sin(angDiffHalf)); final double bevelHalfLen = distance - bevelDelta; // compute the midpoint of the bevel segment final double bevelMidX = basePt.x + mitreDist * Math.cos(mitreMidAng); final double bevelMidY = basePt.y + mitreDist * Math.sin(mitreMidAng); final Coordinate bevelMidPt = new Coordinate(bevelMidX, bevelMidY); // compute the mitre midline segment from the corner point to the bevel segment midpoint final LineSegment mitreMidLine = new LineSegment(basePt, bevelMidPt); // finally the bevel segment endpoints are computed as offsets from // the mitre midline final Coordinate bevelEndLeft = mitreMidLine.pointAlongOffset(1.0, bevelHalfLen); final Coordinate bevelEndRight = mitreMidLine.pointAlongOffset(1.0, -bevelHalfLen); if (m_side == Position.LEFT) { m_bufferBuilder.addVertices(bevelEndLeft); m_bufferBuilder.addVertices(bevelEndRight); } else { m_bufferBuilder.addVertices(bevelEndRight); m_bufferBuilder.addVertices(bevelEndLeft); } }
/** * Add points for a circular fillet around a reflex corner. Adds the start and end points * * @param p base point of curve * @param p0 start point of fillet curve * @param p1 endpoint of fillet curve * @param direction the orientation of the fillet * @param radius the radius of the fillet */ private void addFillet( final Coordinate p, final Coordinate p0, final Coordinate p1, final int direction, final double radius) { final double dx0 = p0.x - p.x; final double dy0 = p0.y - p.y; double startAngle = Math.atan2(dy0, dx0); final double dx1 = p1.x - p.x; final double dy1 = p1.y - p.y; final double endAngle = Math.atan2(dy1, dx1); if (direction == CGAlgorithms.CLOCKWISE) { if (startAngle <= endAngle) startAngle += 2.0 * Math.PI; } else { // direction == COUNTERCLOCKWISE if (startAngle >= endAngle) startAngle -= 2.0 * Math.PI; } m_bufferBuilder.addVertices(p0); addFillet(p, startAngle, endAngle, direction, radius); m_bufferBuilder.addVertices(p1); }
/** * Adds the offset points for an outside (convex) turn * * @param orientation * @param addStartPoint */ private void addOutsideTurn(final int orientation, final boolean addStartPoint) { /** * Heuristic: If offset endpoints are very close together, just use one of them as the corner * vertex. This avoids problems with computing mitre corners in the case where the two segments * are almost parallel (which is hard to compute a robust intersection for). */ if (m_buffer0.p1.distance(m_buffer1.p0) < m_distance1 * OFFSET_SEGMENT_SEPARATION_FACTOR) { m_bufferBuilder.addVertices(m_buffer0.p1); return; } if (m_bufferParams.getJoinStyle() == BufferParameters.JOIN_MITRE) { addMitreJoin(m_segment0.p1, m_buffer0, m_buffer1, m_distance1); } else if (m_bufferParams.getJoinStyle() == BufferParameters.JOIN_BEVEL) { addBevelJoin(m_buffer0, m_buffer1); } else { // add a circular fillet connecting the endpoints of the offset segments if (addStartPoint) m_bufferBuilder.addVertices(m_buffer0.p1); // TESTING - comment out to produce beveled joins addFillet(m_segment0.p1, m_buffer0.p1, m_buffer1.p0, orientation, Math.abs(m_distance1)); m_bufferBuilder.addVertices(m_buffer1.p0); } }
/** * Adds a bevel join connecting the two offset segments around a reflex corner. * * @param offset0 the first offset segment * @param offset1 the second offset segment */ private void addBevelJoin(final LineSegment offset0, final LineSegment offset1) { m_bufferBuilder.addVertices(offset0.p1); m_bufferBuilder.addVertices(offset1.p0); }
/** * Adds the offset points for an inside (concave) turn. * * @param orientation * @param addStartPoint */ private void addInsideTurn() { /** add intersection point of offset segments (if any) */ m_lineIntersector.computeIntersection(m_buffer0.p0, m_buffer0.p1, m_buffer1.p0, m_buffer1.p1); if (m_lineIntersector.hasIntersection()) { m_bufferBuilder.addVertices(m_lineIntersector.getIntersection(0)); } else { /** * If no intersection is detected, it means the angle is so small and/or the offset so large * that the offsets segments don't intersect. In this case we must add a "closing segment" to * make sure the buffer curve is continuous, fairly smooth (e.g. no sharp reversals in * direction) and tracks the buffer correctly around the corner. The curve connects the * endpoints of the segment offsets to points which lie toward the centre point of the corner. * The joining curve will not appear in the final buffer outline, since it is completely * internal to the buffer polygon. In complex buffer cases the closing segment may cut across * many other segments in the generated offset curve. In order to improve the performance of * the noding, the closing segment should be kept as short as possible. (But not too short, * since that would defeat its purpose). This is the purpose of the closingSegFactor heuristic * value. */ /** * The intersection test above is vulnerable to robustness errors; i.e. it may be that the * offsets should intersect very close to their endpoints, but aren't reported as such due to * rounding. To handle this situation appropriately, we use the following test: If the offset * points are very close, don't add closing segments but simply use one of the offset points */ // hasNarrowConcaveAngle = true; // System.out.println("NARROW ANGLE - distance = " + distance); if (m_buffer0.p1.distance(m_buffer1.p0) < m_distance1 * INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR) { m_bufferBuilder.addVertices(m_buffer0.p1); } else { // add endpoint of this segment offset m_bufferBuilder.addVertices(m_buffer0.p1); /** Add "closing segment" of required length. */ if (m_closingSegLengthFactor > 0) { final Coordinate s1 = m_segment0.p1; final Coordinate mid0 = new Coordinate( (m_closingSegLengthFactor * m_buffer0.p1.x + s1.x) / (m_closingSegLengthFactor + 1), (m_closingSegLengthFactor * m_buffer0.p1.y + s1.y) / (m_closingSegLengthFactor + 1)); m_bufferBuilder.addVertices(mid0); final Coordinate mid1 = new Coordinate( (m_closingSegLengthFactor * m_buffer1.p0.x + s1.x) / (m_closingSegLengthFactor + 1), (m_closingSegLengthFactor * m_buffer1.p0.y + s1.y) / (m_closingSegLengthFactor + 1)); m_bufferBuilder.addVertices(mid1); } else { /** * This branch is not expected to be used except for testing purposes. It is equivalent to * the JTS 1.9 logic for closing segments (which results in very poor performance for * large buffer distances) */ m_bufferBuilder.addVertices(m_segment0.p1); } // */ // add start point of next segment offset m_bufferBuilder.addVertices(m_buffer1.p0); } } }