/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.core.ui.model.graphic.imp.area; import static java.lang.Double.NaN; import java.awt.Shape; import java.awt.event.KeyEvent; import java.awt.geom.Area; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; import org.weasis.core.api.gui.util.MathUtil; import org.weasis.core.api.image.measure.MeasurementsAdapter; import org.weasis.core.api.image.util.MeasurableLayer; import org.weasis.core.api.image.util.Unit; import org.weasis.core.ui.Messages; import org.weasis.core.ui.model.graphic.AbstractDragGraphicArea; import org.weasis.core.ui.model.utils.algo.MinimumEnclosingRectangle; import org.weasis.core.ui.model.utils.bean.MeasureItem; import org.weasis.core.ui.model.utils.bean.Measurement; import org.weasis.core.ui.model.utils.exceptions.InvalidShapeException; import org.weasis.core.ui.util.MouseEventDouble; @XmlType(name = "polygon") @XmlRootElement(name = "polygon") public class PolygonGraphic extends AbstractDragGraphicArea { private static final long serialVersionUID = 3835917798842596122L; public static final Integer POINTS_NUMBER = UNDEFINED; public static final Icon ICON = new ImageIcon(PolygonGraphic.class.getResource("/icon/22x22/draw-polygon.png")); //$NON-NLS-1$ public static final Measurement AREA = new Measurement(Messages.getString("measure.area"), 1, true, true, true); //$NON-NLS-1$ public static final Measurement PERIMETER = new Measurement(Messages.getString("measure.perimeter"), 2, true, true, false); //$NON-NLS-1$ public static final Measurement WIDTH = new Measurement(Messages.getString("measure.width"), 3, true, true, false); //$NON-NLS-1$ public static final Measurement HEIGHT = new Measurement(Messages.getString("measure.height"), 4, true, true, false); //$NON-NLS-1$ public static final Measurement TOP_LEFT_POINT_X = new Measurement(Messages.getString("measure.topx"), 5, true, true, false); //$NON-NLS-1$ public static final Measurement TOP_LEFT_POINT_Y = new Measurement(Messages.getString("measure.topy"), 6, true, true, false); //$NON-NLS-1$ public static final Measurement CENTROID_X = new Measurement(Messages.getString("measure.centerx"), 7, true, true, false); //$NON-NLS-1$ public static final Measurement CENTROID_Y = new Measurement(Messages.getString("measure.centery"), 8, true, true, false); //$NON-NLS-1$ public static final Measurement WIDTH_OMBB = new Measurement(Messages.getString("measure.width") + " (OMBB)", 9, false, true, false); //$NON-NLS-1$ //$NON-NLS-2$ public static final Measurement LENGTH_OMBB = new Measurement(Messages.getString("measure.length") + " (OMBB)", 10, false, true, false); //$NON-NLS-1$ //$NON-NLS-2$ public static final Measurement ORIENTATION_OMBB = new Measurement(Messages.getString("measure.orientation") + " (OMBB)", 10, false, true, false); //$NON-NLS-1$ //$NON-NLS-2$ protected static final List<Measurement> MEASUREMENT_LIST = new ArrayList<>(); static { MEASUREMENT_LIST.add(TOP_LEFT_POINT_X); MEASUREMENT_LIST.add(TOP_LEFT_POINT_Y); MEASUREMENT_LIST.add(WIDTH); MEASUREMENT_LIST.add(HEIGHT); MEASUREMENT_LIST.add(CENTROID_X); MEASUREMENT_LIST.add(CENTROID_Y); MEASUREMENT_LIST.add(AREA); MEASUREMENT_LIST.add(PERIMETER); } public PolygonGraphic() { super(POINTS_NUMBER); } public PolygonGraphic(PolygonGraphic graphic) { super(graphic); } @Override public PolygonGraphic copy() { return new PolygonGraphic(this); } @Override public Icon getIcon() { return ICON; } @Override public String getUIName() { return Messages.getString("MeasureToolBar.polygon"); //$NON-NLS-1$ } @Override public int getKeyCode() { return KeyEvent.VK_Y; } @Override public int getModifier() { return 0; } @Override protected void prepareShape() throws InvalidShapeException { // Do not draw points any more setPointNumber(pts.size()); buildShape(null); if (!isShapeValid()) { int lastPointIndex = pts.size() - 1; if (lastPointIndex > 1) { Point2D checkPoint = pts.get(lastPointIndex); /* * Must not have two or several points with the same position at the end of the list (two points is the * convention to have a uncompleted shape when drawing) */ for (int i = lastPointIndex - 1; i >= 0; i--) { if (Objects.equals(checkPoint, pts.get(i))) { pts.remove(i); } else { break; } } // Not useful to close the shape if (Objects.equals(checkPoint, pts.get(0))) { pts.remove(0); } setPointNumber(pts.size()); } if (!isShapeValid() || pts.size() < 3) { throw new InvalidShapeException("This Polygon cannot be drawn"); //$NON-NLS-1$ } buildShape(null); } } @Override public void buildShape(MouseEventDouble mouseEvent) { Shape newShape = null; Optional<Point2D.Double> firstHandlePoint = pts.stream().findFirst(); if (firstHandlePoint.isPresent()) { Point2D.Double p = firstHandlePoint.get(); Path2D polygonPath = new Path2D.Double(Path2D.WIND_NON_ZERO, pts.size()); polygonPath.moveTo(p.getX(), p.getY()); for (Point2D.Double pt : pts) { if (pt == null) { break; } polygonPath.lineTo(pt.getX(), pt.getY()); } polygonPath.closePath(); newShape = polygonPath; } setShape(newShape, mouseEvent); updateLabel(mouseEvent, getDefaultView2d(mouseEvent)); } @Override public boolean isShapeValid() { if (!isGraphicComplete()) { return false; } int lastPointIndex = pts.size() - 1; if (lastPointIndex > 0) { Point2D.Double checkPoint = pts.get(lastPointIndex); if (Objects.equals(checkPoint, pts.get(--lastPointIndex))) { return false; } } return true; } @Override public List<MeasureItem> computeMeasurements(MeasurableLayer layer, boolean releaseEvent, Unit displayUnit) { if (layer != null && layer.hasContent() && isShapeValid()) { MeasurementsAdapter adapter = layer.getMeasurementAdapter(displayUnit); if (adapter != null) { ArrayList<MeasureItem> measVal = new ArrayList<>(12); double ratio = adapter.getCalibRatio(); String unitStr = adapter.getUnit(); Area pathArea = getPathArea(); List<Line2D.Double> lineSegmentList = null; if (TOP_LEFT_POINT_X.getComputed()) { Double val = Optional.ofNullable(pathArea) .map(pa -> adapter.getXCalibratedValue(pa.getBounds2D().getX())).orElse(null); measVal.add(new MeasureItem(TOP_LEFT_POINT_X, val, unitStr)); } if (TOP_LEFT_POINT_Y.getComputed()) { Double val = Optional.ofNullable(pathArea) .map(pa -> adapter.getYCalibratedValue(pa.getBounds2D().getY())).orElse(null); measVal.add(new MeasureItem(TOP_LEFT_POINT_Y, val, unitStr)); } if (WIDTH.getComputed()) { Double val = Optional.ofNullable(pathArea).map(pa -> ratio * pa.getBounds2D().getWidth()).orElse(null); measVal.add(new MeasureItem(WIDTH, val, unitStr)); } if (HEIGHT.getComputed()) { Double val = Optional.ofNullable(pathArea).map(pa -> ratio * pa.getBounds2D().getHeight()).orElse(null); measVal.add(new MeasureItem(HEIGHT, val, unitStr)); } Point2D centroid = null; if (CENTROID_X.getComputed()) { if (lineSegmentList == null) { lineSegmentList = getClosedPathSegments(pathArea); } centroid = (centroid == null) ? getCentroid(lineSegmentList) : centroid; Double val = (centroid != null) ? adapter.getXCalibratedValue( centroid.getX()) : null; measVal.add(new MeasureItem(CENTROID_X, val, unitStr)); } if (CENTROID_Y.getComputed()) { if (lineSegmentList == null) { lineSegmentList = getClosedPathSegments(pathArea); } centroid = (centroid == null) ? getCentroid(lineSegmentList) : centroid; Double val = (centroid != null) ? adapter.getYCalibratedValue( centroid.getY()) : null; measVal.add(new MeasureItem(CENTROID_Y, val, unitStr)); } if (AREA.getComputed()) { if (lineSegmentList == null) { lineSegmentList = getClosedPathSegments(pathArea); } Double val = (lineSegmentList != null) ? getAreaValue(lineSegmentList) * ratio * ratio : null; String unit = "pix".equals(unitStr) ? unitStr : unitStr + "2"; //$NON-NLS-1$ //$NON-NLS-2$ measVal.add(new MeasureItem(AREA, val, unit)); } if (PERIMETER.getComputed()) { if (lineSegmentList == null) { lineSegmentList = getClosedPathSegments(pathArea); } Double val = (lineSegmentList != null) ? getPerimeter(lineSegmentList) * ratio : null; measVal.add(new MeasureItem(PERIMETER, val, unitStr)); } if (releaseEvent && (WIDTH_OMBB.getComputed() || LENGTH_OMBB.getComputed())) { Double l = null; Double w = null; Double o = null; MinimumEnclosingRectangle rect = new MinimumEnclosingRectangle(pts, false); List<java.awt.geom.Point2D.Double> minRect = rect.getMinimumRectangle(); if (minRect.size() == 4) { l = ratio * minRect.get(0).distance(minRect.get(1)); w = ratio * minRect.get(1).distance(minRect.get(2)); o = MathUtil.getOrientation(minRect.get(0), minRect.get(1)); if (l < w) { double tmp = l; l = w; w = tmp; o = MathUtil.getOrientation(minRect.get(1), minRect.get(2)); } } measVal.add(new MeasureItem(LENGTH_OMBB, l, unitStr)); measVal.add(new MeasureItem(WIDTH_OMBB, w, unitStr)); measVal.add(new MeasureItem(ORIENTATION_OMBB, o, Messages.getString("measure.deg"))); //$NON-NLS-1$ } List<MeasureItem> stats = getImageStatistics(layer, releaseEvent); if (stats != null) { measVal.addAll(stats); } return measVal; } } return Collections.emptyList(); } @Override public List<Measurement> getMeasurementList() { return MEASUREMENT_LIST; } /** * Construct a polygon Area which represents a non-self-intersecting shape using a path Winding Rule : WIND_NON_ZERO * * @return area of the closed polygon, or null if shape is invalid */ protected final Area getPathArea() { Optional<Point2D.Double> firstHandlePoint = pts.stream().findFirst(); if (firstHandlePoint.isPresent()) { Point2D.Double p = firstHandlePoint.get(); Path2D polygonPath = new Path2D.Double(Path2D.WIND_NON_ZERO, pts.size()); polygonPath.moveTo(p.getX(), p.getY()); for (Point2D.Double pt : pts) { if (pt == null) { return null; } polygonPath.lineTo(pt.getX(), pt.getY()); } return new Area(polygonPath); } return null; } /** * Construct a list of line segments which defines the outside path of a given polygon Area with each vertices * ordered in the same direction<br> * * @return list of line segments around the closed polygon, or null if shape is invalid */ public final List<Line2D.Double> getClosedPathSegments() { return getClosedPathSegments(getPathArea()); } protected final List<Line2D.Double> getClosedPathSegments(Area pathArea) { List<Line2D.Double> lineSegmentList = null; if (pathArea != null) { lineSegmentList = new ArrayList<>(pts.size()); PathIterator pathIt = pathArea.getPathIterator(null); double[] coords = new double[6]; Double startX = NaN; Double startY = NaN; Double curX = NaN; Double curY = NaN; Set<Point2D.Double> ptSet = new HashSet<>(lineSegmentList.size() * 2); while (!pathIt.isDone()) { Integer segType = pathIt.currentSegment(coords); Double lastX = coords[0]; Double lastY = coords[1]; switch (segType) { case PathIterator.SEG_CLOSE: lastX = startX; lastY = startY; case PathIterator.SEG_LINETO: Point2D.Double ptP1 = new Point2D.Double(curX, curY); Point2D.Double ptP2 = new Point2D.Double(lastX, lastY); BigDecimal dist = BigDecimal.valueOf(ptP1.distance(ptP2)).setScale(10, RoundingMode.DOWN); if (dist.compareTo(BigDecimal.ZERO) != 0) { for (Point2D.Double pt : new Point2D.Double[] { ptP1, ptP2 }) { boolean newPt = true; for (Point2D.Double p : ptSet) { dist = BigDecimal.valueOf(p.distance(pt)).setScale(10, RoundingMode.DOWN); if (dist.compareTo(BigDecimal.ZERO) == 0) { pt.setLocation(p); newPt = false; break; } } if (newPt) { ptSet.add(pt); } } lineSegmentList.add(new Line2D.Double(ptP1, ptP2)); } curX = lastX; curY = lastY; break; case PathIterator.SEG_MOVETO: startX = curX = lastX; startY = curY = lastY; break; } pathIt.next(); } } return lineSegmentList; } /** * @return perimeter the closed polygon, or null if shape is invalid */ public Double getPerimeter() { return getPerimeter(getClosedPathSegments()); } protected Double getPerimeter(List<Line2D.Double> lineSegmentList) { if (lineSegmentList != null) { double perimeter = 0.0; for (Line2D line : lineSegmentList) { perimeter += line.getP1().distance(line.getP2()); } return perimeter; } return null; } /** * The centroid (a.k.a. the center of mass, or center of gravity) of a polygon can be computed as the weighted sum * of the centroids of a partition of the polygon into triangles. <br> * This suggests first triangulating the polygon, then forming a sum of the centroids of each triangle, weighted by * the area of each triangle, the whole sum normalized by the total polygon area. <br> * <br> * Simpler method: the triangulation need not be a partition, but rather can use positively and negatively oriented * triangles (with positive and negative areas), as is used when computing the area of a polygon. Then, simple * algorithm for computing the centroid is based on a sum of triangle centroids weighted with their signed area. The * triangles can be taken to be those formed by one fixed vertex v0 of the polygon, and the two endpoints of * consecutive edges of the polygon: (v1,v2), (v2,v3), etc.<br> * * @return position of the centroid assuming the polygon is closed, or null if shape is not valid */ public Point2D getCentroid() { return getCentroid(getClosedPathSegments()); } protected Point2D getCentroid(List<Line2D.Double> lineSegmentList) { if (lineSegmentList != null) { Double area = 0d; Double cx = 0d; Double cy = 0d; for (Line2D.Double line : lineSegmentList) { Point2D.Double p1 = (Point2D.Double) line.getP1(); Point2D.Double p2 = (Point2D.Double) line.getP2(); double tmp = (p1.getX() * p2.getY()) - (p2.getX() * p1.getY()); area += tmp; cx += (p1.getX() + p2.getX()) * tmp; cy += (p1.getY() + p2.getY()) * tmp; } area /= 2.0; cx /= (6.0 * area); cy /= (6.0 * area); return new Point2D.Double(cx, cy); } return null; } /** * <b>Algorithm</b><br> * <br> * -1- List the x and y coordinates of each vertex of the polygon in counterclockwise order about the normal. Repeat * the coordinates of the first point at the end of the list. <br> * -2- Multiply the x coordinate of each vertex by the y coordinate of the next vertex.<br> * -3- Multiply the y coordinate of each vertex by the x coordinate of the next vertex <br> * -4- Subtract the sum of the products computed in step 3 from the sum of the products from step 2 <br> * -5- Divide this difference by 2 to get the area of the polygon. <br> * <br> * <b> Warning </b><br> * <br> * This formula computes area with orientation. When listing the points in a clockwise order instead of * counterclockwise, result is the negative of the area. <br> * The method produces the wrong answer for crossed polygons, where one side crosses over another. <br> * For instance, when two lines of the drawing path cross like a figure eight, result is the area surrounded * counterclockwise minus the area surrounded clockwise.<br> * It works correctly however for triangles, regular, irregular, convex and concave polygons. <br> * <br> * Solution is to compute area only from the outside path of the polygon with each vertices ordered in the same * direction.<br> * This can be achieved trough Area() constructors which decompose the shape into non-self-intersecting shape. */ public Double getAreaValue() { return getAreaValue(getClosedPathSegments()); } protected Double getAreaValue(List<Line2D.Double> lineSegmentList) { if (lineSegmentList != null) { Double area = 0d; for (Line2D.Double line : lineSegmentList) { Point2D.Double p1 = (Point2D.Double) line.getP1(); Point2D.Double p2 = (Point2D.Double) line.getP2(); area += (p1.getX() * p2.getY()) - (p2.getX() * p1.getY()); } return Math.abs(area) / 2.0; } return null; } }