001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package plugins.kernel.roi.roi2d;
020
021import icy.painter.Anchor2D;
022import icy.painter.PathAnchor2D;
023import icy.resource.ResourceUtil;
024import icy.roi.ROI;
025import icy.type.point.Point5D;
026import icy.util.ShapeUtil;
027import icy.util.XMLUtil;
028
029import java.awt.Shape;
030import java.awt.geom.Area;
031import java.awt.geom.Path2D;
032import java.awt.geom.Point2D;
033import java.awt.geom.Rectangle2D;
034import java.util.ArrayList;
035import java.util.List;
036
037import org.w3c.dom.Element;
038import org.w3c.dom.Node;
039
040/**
041 * ROI Path.<br>
042 * This ROI can display a Path2D shape.<br>
043 * You can modify and remove points (adding new point isn't supported).
044 * 
045 * @author Stephane
046 */
047public class ROI2DPath extends ROI2DShape
048{
049    public static final String ID_POINTS = "points";
050    public static final String ID_POINT = "point";
051    public static final String ID_WINDING = "winding";
052
053    protected Area closedArea;
054    protected Path2D openPath;
055
056    static Path2D initPath(Point2D position)
057    {
058        final Path2D result = new Path2D.Double();
059
060        result.reset();
061        if (position != null)
062            result.moveTo(position.getX(), position.getY());
063
064        return result;
065    }
066
067    /**
068     * Build a new ROI2DPath from the specified path.
069     */
070    public ROI2DPath(Path2D path, Area closedArea, Path2D openPath)
071    {
072        super(path);
073
074        rebuildControlPointsFromPath();
075
076        if (closedArea == null)
077            this.closedArea = new Area(ShapeUtil.getClosedPath(path));
078        else
079            this.closedArea = closedArea;
080        if (openPath == null)
081            this.openPath = ShapeUtil.getOpenPath(path);
082        else
083            this.openPath = openPath;
084
085        // set icon (default name is defined by getDefaultName())
086        setIcon(ResourceUtil.ICON_ROI_POLYLINE);
087    }
088
089    /**
090     * Build a new ROI2DPath from the specified path.
091     */
092    public ROI2DPath(Path2D path)
093    {
094        this(path, null, null);
095
096    }
097
098    /**
099     * Build a new ROI2DPath from the specified path.
100     */
101    public ROI2DPath(Shape shape)
102    {
103        this(new Path2D.Double(shape), (shape instanceof Area) ? (Area) shape : null, null);
104    }
105
106    /**
107     * @deprecated
108     */
109    @Deprecated
110    public ROI2DPath(Point2D pt, boolean cm)
111    {
112        this(pt);
113    }
114
115    public ROI2DPath(Point2D position)
116    {
117        this(initPath(position));
118    }
119
120    /**
121     * Generic constructor for interactive mode
122     */
123    public ROI2DPath(Point5D pt)
124    {
125        this(pt.toPoint2D());
126    }
127
128    public ROI2DPath()
129    {
130        this(new Path2D.Double(Path2D.WIND_NON_ZERO));
131    }
132
133    @Override
134    public String getDefaultName()
135    {
136        return "Path2D";
137    }
138
139    @Override
140    protected Anchor2D createAnchor(Point2D pos)
141    {
142        return new PathAnchor2D(pos.getX(), pos.getY(), getColor(), getFocusedColor());
143    }
144
145    protected void rebuildControlPointsFromPath()
146    {
147        beginUpdate();
148        try
149        {
150            // remove all point
151            removeAllPoint();
152
153            // add path points to the control point list
154            for (Anchor2D pt : ShapeUtil.getAnchorsFromShape(getPath(), getColor(), getFocusedColor()))
155                addPoint(pt);
156        }
157        finally
158        {
159            endUpdate();
160        }
161    }
162
163    protected Path2D getPath()
164    {
165        return (Path2D) shape;
166    }
167
168    /**
169     * Returns the closed area part of the ROI2DPath in {@link Area} shape format
170     */
171    public Area getClosedArea()
172    {
173        return closedArea;
174    }
175
176    /**
177     * Returns the open path part of the ROI2DPath in {@link Path2D} shape format
178     */
179    public Path2D getOpenPath()
180    {
181        return openPath;
182    }
183
184    @Override
185    public boolean canAddPoint()
186    {
187        // this ROI doesn't support point add
188        return false;
189    }
190
191    @Override
192    public boolean contains(double x, double y)
193    {
194        // only consider closed path
195        return ShapeUtil.getClosedPath(getPath()).contains(x, y);
196    }
197
198    @Override
199    public boolean contains(Point2D p)
200    {
201        // only consider closed path
202        return ShapeUtil.getClosedPath(getPath()).contains(p);
203    }
204
205    @Override
206    public boolean contains(double x, double y, double w, double h)
207    {
208        // only consider closed path
209        return ShapeUtil.getClosedPath(getPath()).contains(x, y, w, h);
210    }
211
212    @Override
213    public boolean contains(Rectangle2D r)
214    {
215        // only consider closed path
216        return ShapeUtil.getClosedPath(getPath()).contains(r);
217    }
218
219    @Override
220    public boolean contains(ROI roi)
221    {
222        // not closed --> do not contains anything
223        if (!ShapeUtil.isClosed(shape))
224            return false;
225
226        return super.contains(roi);
227    }
228
229    @Override
230    public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException
231    {
232        if (roi instanceof ROI2DShape)
233        {
234            final ROI2DShape roiShape = (ROI2DShape) roi;
235
236            // only if on same position
237            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
238            {
239                final Path2D path = getPath();
240
241                if (roi instanceof ROI2DPath)
242                {
243                    final ROI2DPath roiPath = (ROI2DPath) roi;
244
245                    // compute closed area and open path parts
246                    closedArea.add(roiPath.closedArea);
247                    openPath.append(roiPath.openPath, false);
248                }
249                else
250                {
251                    // compute closed area and open path parts
252                    if (roiShape.getShape() instanceof Area)
253                        closedArea.add((Area) roiShape.getShape());
254                    else
255                        closedArea.add(new Area(ShapeUtil.getClosedPath(roiShape)));
256                    openPath.append(ShapeUtil.getOpenPath(roiShape), false);
257                }
258
259                // then rebuild path from closed and open parts
260                path.reset();
261                path.append(closedArea, false);
262                path.append(openPath, false);
263
264                rebuildControlPointsFromPath();
265                roiChanged(true);
266
267                return this;
268            }
269        }
270
271        return super.add(roi, allowCreate);
272    }
273
274    @Override
275    public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException
276    {
277        if (roi instanceof ROI2DShape)
278        {
279            final ROI2DShape roiShape = (ROI2DShape) roi;
280
281            // only if on same position
282            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
283            {
284                final Path2D path = getPath();
285
286                if (roi instanceof ROI2DPath)
287                {
288                    final ROI2DPath roiPath = (ROI2DPath) roi;
289
290                    // compute closed area intersection and clear open path
291                    closedArea.intersect(roiPath.closedArea);
292                    openPath.reset();
293                }
294                else
295                {
296                    // compute closed area intersection and clear open path
297                    if (roiShape.getShape() instanceof Area)
298                        closedArea.intersect((Area) roiShape.getShape());
299                    else
300                        closedArea.intersect(new Area(ShapeUtil.getClosedPath(roiShape)));
301                    openPath.reset();
302                }
303
304                // then rebuild path from closed area (open part is empty)
305                path.reset();
306                path.append(closedArea, false);
307
308                rebuildControlPointsFromPath();
309                roiChanged(true);
310
311                return this;
312            }
313        }
314
315        return super.intersect(roi, allowCreate);
316    }
317
318    @Override
319    public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException
320    {
321        if (roi instanceof ROI2DShape)
322        {
323            final ROI2DShape roiShape = (ROI2DShape) roi;
324
325            // only if on same position
326            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
327            {
328                final Path2D path = getPath();
329
330                if (roi instanceof ROI2DPath)
331                {
332                    final ROI2DPath roiPath = (ROI2DPath) roi;
333
334                    // compute exclusive union on closed area and simple append for open path
335                    closedArea.exclusiveOr(roiPath.closedArea);
336                    openPath.append(roiPath.openPath, false);
337                }
338                else
339                {
340                    // compute exclusive union on closed area and simple append for open path
341                    if (roiShape.getShape() instanceof Area)
342                        closedArea.exclusiveOr((Area) roiShape.getShape());
343                    else
344                        closedArea.exclusiveOr(new Area(ShapeUtil.getClosedPath(roiShape)));
345                    openPath.append(ShapeUtil.getOpenPath(roiShape), false);
346                }
347
348                // then rebuild path from closed and open parts
349                path.reset();
350                path.append(closedArea, false);
351                path.append(openPath, false);
352
353                rebuildControlPointsFromPath();
354                roiChanged(true);
355
356                return this;
357            }
358        }
359
360        return super.exclusiveAdd(roi, allowCreate);
361    }
362
363    @Override
364    public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException
365    {
366        if (roi instanceof ROI2DShape)
367        {
368            final ROI2DShape roiShape = (ROI2DShape) roi;
369
370            // only if on same position
371            if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC()))
372            {
373                final Path2D path = getPath();
374
375                if (roi instanceof ROI2DPath)
376                {
377                    final ROI2DPath roiPath = (ROI2DPath) roi;
378
379                    // compute closed area intersection and clear open path parts
380                    closedArea.exclusiveOr(roiPath.closedArea);
381                    if (!roiPath.closedArea.isEmpty())
382                        openPath.reset();
383                }
384                else
385                {
386                    final Area area;
387
388                    // compute closed area and open path parts
389                    if (roiShape.getShape() instanceof Area)
390                        area = (Area) roiShape.getShape();
391                    else
392                        area = new Area(ShapeUtil.getClosedPath(roiShape));
393                    if (!area.isEmpty())
394                    {
395                        closedArea.exclusiveOr(area);
396                        openPath.reset();
397                    }
398                }
399
400                // then rebuild path from closed and open parts
401                path.reset();
402                path.append(closedArea, false);
403                path.append(openPath, false);
404
405                rebuildControlPointsFromPath();
406                roiChanged(true);
407
408                return this;
409            }
410        }
411
412        return super.subtract(roi, allowCreate);
413    }
414
415    /**
416     * Return the list of control points for this ROI.
417     */
418    public List<PathAnchor2D> getPathAnchors()
419    {
420        final List<PathAnchor2D> result = new ArrayList<PathAnchor2D>();
421
422        synchronized (controlPoints)
423        {
424            for (Anchor2D pt : controlPoints)
425                result.add((PathAnchor2D) pt);
426        }
427
428        return result;
429    }
430
431    protected void updateCachedStructures()
432    {
433        closedArea = new Area(ShapeUtil.getClosedPath(getPath()));
434        openPath = ShapeUtil.getOpenPath(getPath());
435    }
436
437    @Override
438    protected void updateShape()
439    {
440        ShapeUtil.buildPathFromAnchors(getPath(), getPathAnchors(), false);
441        // update internal closed area and open path
442        updateCachedStructures();
443
444        // call super method after shape has been updated
445        super.updateShape();
446    }
447
448    @Override
449    public boolean loadFromXML(Node node)
450    {
451        beginUpdate();
452        try
453        {
454            if (!super.loadFromXML(node))
455                return false;
456
457            removeAllPoint();
458
459            final List<Node> nodesPoint = XMLUtil.getChildren(XMLUtil.getElement(node, ID_POINTS), ID_POINT);
460            if (nodesPoint != null)
461            {
462                for (Node n : nodesPoint)
463                {
464                    final PathAnchor2D pt = (PathAnchor2D) createAnchor(new Point2D.Double());
465                    pt.loadPositionFromXML(n);
466                    addPoint(pt);
467                }
468            }
469
470            getPath().setWindingRule(XMLUtil.getElementIntValue(node, ID_WINDING, Path2D.WIND_NON_ZERO));
471        }
472        finally
473        {
474            endUpdate();
475        }
476
477        return true;
478    }
479
480    @Override
481    public boolean saveToXML(Node node)
482    {
483        if (!super.saveToXML(node))
484            return false;
485
486        final Element points = XMLUtil.setElement(node, ID_POINTS);
487
488        synchronized (controlPoints)
489        {
490            for (Anchor2D pt : controlPoints)
491                pt.savePositionToXML(XMLUtil.addElement(points, ID_POINT));
492        }
493
494        XMLUtil.setElementIntValue(node, ID_WINDING, getPath().getWindingRule());
495
496        return true;
497    }
498}