/******************************************************************************* * 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; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.swing.JOptionPane; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlElements; import javax.xml.bind.annotation.XmlType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.gui.util.ActionW; import org.weasis.core.api.image.util.MeasurableLayer; import org.weasis.core.api.media.data.ImageElement; import org.weasis.core.ui.Messages; import org.weasis.core.ui.editor.image.Canvas; import org.weasis.core.ui.editor.image.DefaultView2d; import org.weasis.core.ui.editor.image.MeasureToolBar; import org.weasis.core.ui.editor.image.ViewCanvas; import org.weasis.core.ui.model.graphic.DragGraphic; import org.weasis.core.ui.model.graphic.Graphic; import org.weasis.core.ui.model.graphic.GraphicLabel; import org.weasis.core.ui.model.graphic.GraphicSelectionListener; import org.weasis.core.ui.model.graphic.imp.AnnotationGraphic; import org.weasis.core.ui.model.graphic.imp.PixelInfoGraphic; import org.weasis.core.ui.model.graphic.imp.PointGraphic; import org.weasis.core.ui.model.graphic.imp.angle.AngleToolGraphic; import org.weasis.core.ui.model.graphic.imp.angle.CobbAngleToolGraphic; import org.weasis.core.ui.model.graphic.imp.angle.FourPointsAngleToolGraphic; import org.weasis.core.ui.model.graphic.imp.angle.OpenAngleToolGraphic; import org.weasis.core.ui.model.graphic.imp.area.EllipseGraphic; import org.weasis.core.ui.model.graphic.imp.area.PolygonGraphic; import org.weasis.core.ui.model.graphic.imp.area.RectangleGraphic; import org.weasis.core.ui.model.graphic.imp.area.SelectGraphic; import org.weasis.core.ui.model.graphic.imp.area.ThreePointsCircleGraphic; import org.weasis.core.ui.model.graphic.imp.line.LineGraphic; import org.weasis.core.ui.model.graphic.imp.line.LineWithGapGraphic; import org.weasis.core.ui.model.graphic.imp.line.ParallelLineGraphic; import org.weasis.core.ui.model.graphic.imp.line.PerpendicularLineGraphic; import org.weasis.core.ui.model.graphic.imp.line.PolylineGraphic; import org.weasis.core.ui.model.layer.GraphicLayer; import org.weasis.core.ui.model.layer.GraphicModelChangeListener; import org.weasis.core.ui.model.layer.LayerType; import org.weasis.core.ui.model.layer.imp.DefaultLayer; import org.weasis.core.ui.model.utils.imp.DefaultUUID; import org.weasis.core.ui.util.MouseEventDouble; @XmlType(propOrder = { "referencedSeries", "layers", "models" }) @XmlAccessorType(XmlAccessType.NONE) public abstract class AbstractGraphicModel extends DefaultUUID implements GraphicModel { private static final long serialVersionUID = 1187916695295007387L; private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGraphicModel.class); private volatile List<ReferencedSeries> referencedSeries; private volatile List<GraphicLayer> layers; protected volatile List<Graphic> models; private final List<GraphicSelectionListener> selectedGraphicsListeners = new ArrayList<>(); private final List<GraphicModelChangeListener> modelListeners = new ArrayList<>(); private final List<PropertyChangeListener> graphicsListeners = new ArrayList<>(); private Boolean changeFireingSuspended = Boolean.FALSE; private Function<Graphic, GraphicLayer> getLayer = g -> g.getLayer(); private Function<Graphic, DragGraphic> castToDragGraphic = DragGraphic.class::cast; private Predicate<Graphic> isLayerVisible = g -> g.getLayer().getVisible(); private Predicate<Graphic> isGraphicSelected = g -> g.getSelected(); public AbstractGraphicModel() { this(null); } public AbstractGraphicModel(List<ReferencedSeries> referencedSeries) { setReferencedSeries(referencedSeries); this.layers = Collections.synchronizedList(new ArrayList<>()); this.models = Collections.synchronizedList(new ArrayList<>()); } @XmlElementWrapper(name = "graphics") @XmlElements({ @XmlElement(name = "point", type = PointGraphic.class), @XmlElement(name = "angle", type = AngleToolGraphic.class), @XmlElement(name = "annotation", type = AnnotationGraphic.class), @XmlElement(name = "pixelInfo", type = PixelInfoGraphic.class), @XmlElement(name = "openAngle", type = OpenAngleToolGraphic.class), @XmlElement(name = "cobbAngle", type = CobbAngleToolGraphic.class), @XmlElement(name = "rectangle", type = RectangleGraphic.class), @XmlElement(name = "ellipse", type = EllipseGraphic.class), @XmlElement(name = "fourPointsAngle", type = FourPointsAngleToolGraphic.class), @XmlElement(name = "line", type = LineGraphic.class), @XmlElement(name = "lineWithGap", type = LineWithGapGraphic.class), @XmlElement(name = "perpendicularLine", type = PerpendicularLineGraphic.class), @XmlElement(name = "parallelLine", type = ParallelLineGraphic.class), @XmlElement(name = "polygon", type = PolygonGraphic.class), @XmlElement(name = "polyline", type = PolylineGraphic.class), @XmlElement(name = "threePointsCircle", type = ThreePointsCircleGraphic.class) }) @Override public List<Graphic> getModels() { return models; } @XmlElementWrapper(name = "layers") @XmlElements({ @XmlElement(name = "layer", type = DefaultLayer.class) }) @Override public List<GraphicLayer> getLayers() { return layers; } @XmlElementWrapper(name = "references") @XmlElement(name = "series") @Override public List<ReferencedSeries> getReferencedSeries() { return referencedSeries; } @Override public void setReferencedSeries(List<ReferencedSeries> referencedSeries) { if (referencedSeries != null && !referencedSeries.getClass().getSimpleName().startsWith("Synchronized")) { //$NON-NLS-1$ this.referencedSeries = Collections.synchronizedList(referencedSeries); } this.referencedSeries = Optional.ofNullable(referencedSeries).orElseGet(() -> Collections.synchronizedList(new ArrayList<>())); } @Override public void setModels(List<Graphic> models) { if (models != null) { this.models = Collections.synchronizedList(models); this.layers = Collections.synchronizedList(getLayerlist()); } } @Override public void addGraphic(Graphic graphic) { if (graphic != null) { GraphicLayer layer = graphic.getLayer(); if (layer == null) { layer = findLayerByType(graphic.getLayerType()).orElseGet(() -> new DefaultLayer(graphic.getLayerType())); graphic.setLayer(layer); } if (!layers.contains(layer)) { layers.add(layer); } models.add(graphic); } } @Override public void removeGraphic(Graphic graphic) { if (graphic != null) { models.remove(graphic); graphic.removeAllPropertyChangeListener(); GraphicLayer layer = graphic.getLayer(); if (layer != null) { boolean layerExist = false; synchronized (models) { for (Graphic g : models) { if (g.getLayer().equals(layer)) { layerExist = true; break; } } } if (!layerExist) { layers.remove(layer); } } } } private List<GraphicLayer> getLayerlist() { return models.parallelStream().map(getLayer).distinct().collect(Collectors.toList()); } @Override public void addGraphicChangeHandler(PropertyChangeListener graphicsChangeHandler) { if (Objects.nonNull(graphicsChangeHandler) && !graphicsListeners.contains(graphicsChangeHandler)) { graphicsListeners.add(graphicsChangeHandler); models.forEach(g -> g.addPropertyChangeListener(graphicsChangeHandler)); } } @Override public void removeGraphicChangeHandler(PropertyChangeListener graphicsChangeHandler) { if (Objects.nonNull(graphicsChangeHandler) && graphicsListeners.contains(graphicsChangeHandler)) { graphicsListeners.remove(graphicsChangeHandler); models.forEach(g -> g.removePropertyChangeListener(graphicsChangeHandler)); } } @Override public List<PropertyChangeListener> getGraphicsListeners() { return graphicsListeners; } @Override public void updateLabels(Object source, ViewCanvas<? extends ImageElement> view) { models.forEach(g -> g.updateLabel(source, view)); } @Override public Optional<GraphicLayer> findLayerByType(LayerType type) { Objects.requireNonNull(type); return layers.stream().filter(isLayerTypeEquals(type)).findFirst(); } @Override public List<GraphicLayer> groupLayerByType() { if (models.isEmpty()) { return Collections.emptyList(); } ArrayList<GraphicLayer> layerType = new ArrayList<>(); synchronized (models) { for (Graphic g : models) { LayerType type = g.getLayer().getType(); boolean notInGroup = true; for (GraphicLayer glayer : layerType) { if (Objects.equals(glayer.getType(), type)) { notInGroup = false; break; } } if (notInGroup) { layerType.add(g.getLayer()); } } } return layerType; } @Override public void deleteByLayer(GraphicLayer layer) { Objects.requireNonNull(layer); if (models.isEmpty()) { return; } synchronized (models) { models.removeIf(g -> { boolean delete = layer.equals(g.getLayer()); if (delete) { g.removeAllPropertyChangeListener(); } return delete; }); layers.removeIf(l -> Objects.equals(l, layer)); } } @Override public void deleteByLayerType(LayerType type) { Objects.requireNonNull(type); if (models.isEmpty()) { return; } synchronized (models) { for (Graphic g : models) { if (g.getLayer().getType().equals(type)) { g.removeAllPropertyChangeListener(); } } models.removeIf(g -> Objects.equals(g.getLayer().getType(), type)); layers.removeIf(l -> Objects.equals(l.getType(), type)); } } @Override public void deleteNonSerializableGraphics() { if (models.isEmpty()) { return; } synchronized (models) { for (Graphic g : models) { if (!g.getLayer().getSerializable()) { g.removeAllPropertyChangeListener(); } } models.removeIf(g -> !g.getLayer().getSerializable()); layers.removeIf(l -> !l.getSerializable()); } } @Override public boolean hasSerializableGraphics() { if (models.isEmpty()) { return false; } synchronized (models) { for (Graphic g : models) { /* * Exclude non serializable layer and graphics without points like NonEditableGraphic (not strictly the * jaxb serialization process that use the annotations from getModels()) */ if (g.getLayer().getSerializable() && !g.getPts().isEmpty()) { return true; } } } return false; } @Override public List<Graphic> getAllGraphics() { return models.stream().filter(isLayerVisible).collect(Collectors.toList()); } @Override public List<Graphic> getSelectedAllGraphicsIntersecting(Rectangle rectangle, AffineTransform transform) { ArrayList<Graphic> selectedGraphicList = new ArrayList<>(); if (rectangle != null) { synchronized (models) { for (int i = models.size() - 1; i >= 0; i--) { Graphic graphic = models.get(i); GraphicLayer layer = graphic.getLayer(); if (layer.getVisible() && layer.getSelectable()) { Rectangle graphBounds = graphic.getBounds(transform); if (graphBounds != null && graphBounds.intersects(rectangle)) { Area selectionArea = graphic.getArea(transform); if (selectionArea != null && selectionArea.intersects(rectangle)) { selectedGraphicList.add(graphic); continue; } } GraphicLabel graphicLabel = graphic.getGraphicLabel(); if (graphic.getLabelVisible() && graphicLabel != null && graphicLabel.getLabels() != null) { Area selectionArea = graphicLabel.getArea(transform); if (selectionArea != null && selectionArea.intersects(rectangle)) { selectedGraphicList.add(graphic); } } } } } } return selectedGraphicList; } @Override public List<Graphic> getSelectedAllGraphicsIntersecting(Rectangle rectangle, AffineTransform transform, boolean onlyFrontGraphic) { ArrayList<Graphic> selectedGraphicList = new ArrayList<>(); if (rectangle != null) { synchronized (models) { for (int i = models.size() - 1; i >= 0; i--) { Graphic graphic = models.get(i); GraphicLayer layer = graphic.getLayer(); if (layer.getVisible() && layer.getSelectable()) { List<Area> selectedAreaList = new ArrayList<>(); Area selectedArea = null; Rectangle selectionBounds = graphic.getRepaintBounds(transform); if (selectionBounds != null && selectionBounds.intersects(rectangle)) { selectedArea = graphic.getArea(transform); } GraphicLabel graphicLabel = graphic.getGraphicLabel(); if (graphicLabel != null && graphicLabel.getLabels() != null) { Area labelArea = graphicLabel.getArea(transform); if (labelArea != null) { if (selectedArea != null) { selectedArea.add(labelArea); } else if (labelArea.intersects(rectangle)) { selectedArea = graphic.getArea(transform); selectedArea.add(labelArea); } } } if (selectedArea != null) { if (onlyFrontGraphic) { for (Area area : selectedAreaList) { selectedArea.subtract(area);// subtract any areas from front graphics // already selected } } if (selectedArea.intersects(rectangle)) { selectedAreaList.add(selectedArea); selectedGraphicList.add(graphic); } } } } } } return selectedGraphicList; } /** * @param mouseevent * @return first selected graphic intersecting if exist, otherwise simply first graphic intersecting, or null */ @Override public Optional<Graphic> getFirstGraphicIntersecting(MouseEventDouble mouseEvent) { final Point2D mousePt = mouseEvent.getImageCoordinates(); Graphic firstSelectedGraph = null; synchronized (models) { for (int i = models.size() - 1; i >= 0; i--) { Graphic g = models.get(i); GraphicLayer l = g.getLayer(); if (l.getVisible() && l.getSelectable()) { if (g.isOnGraphicLabel(mouseEvent)) { if (g.getSelected()) { return Optional.of(g); } else if (firstSelectedGraph == null) { firstSelectedGraph = g; } } // Improve speed by checking if mousePoint is inside repaintBound before checking if inside Area Rectangle2D repaintBound = g.getRepaintBounds(mouseEvent); if (repaintBound != null && repaintBound.contains(mousePt)) { if ((g.getHandlePointIndex(mouseEvent) >= 0) || (g.getArea(mouseEvent).contains(mousePt))) { if (g.getSelected()) { return Optional.of(g); } else if (firstSelectedGraph == null) { firstSelectedGraph = g; } } } } } } return Optional.ofNullable(firstSelectedGraph); } // @Override // public List<Graphic> getGraphicsBoundsInArea(Rectangle rect) { // List<Graphic> arraylist = new ArrayList<>(); // if (graphics != null && rect != null) { // for (int j = graphics.list.size() - 1; j >= 0; j--) { // Graphic graphic = graphics.list.get(j); // Rectangle2D graphicBounds = graphic.getRepaintBounds(getAffineTransform()); // if (graphicBounds != null && graphicBounds.intersects(rect)) { // arraylist.add(graphic); // } // } // } // return arraylist; // } // @Override // public AbstractDragGraphic getGraphicContainPoint(MouseEventDouble mouseEvt) { // final Point2D mousePt = mouseEvt.getImageCoordinates(); // // if (graphics != null && mousePt != null) { // // for (int j = graphics.list.size() - 1; j >= 0; j--) { // if (graphics.list.get(j) instanceof AbstractDragGraphic) { // // AbstractDragGraphic dragGraph = (AbstractDragGraphic) graphics.list.get(j); // // if (dragGraph.isOnGraphicLabel(mouseEvt)) { // return dragGraph; // } // // // Improve speed by checking if mousePoint is inside repaintBound before checking if inside Area // Rectangle2D repaintBound = dragGraph.getRepaintBounds(mouseEvt); // if (repaintBound != null && repaintBound.contains(mousePt)) { // if ((dragGraph.getHandlePointIndex(mouseEvt) >= 0) // || (dragGraph.getArea(mouseEvt).contains(mousePt))) { // return dragGraph; // } // } // } // } // } // return null; // } @Override public List<DragGraphic> getSelectedDragableGraphics() { return models.stream().filter(isGraphicSelected).filter(DragGraphic.class::isInstance).map(castToDragGraphic) .collect(Collectors.toList()); } @Override public List<Graphic> getSelectedGraphics() { return models.stream().filter(isGraphicSelected).collect(Collectors.toList()); } @Override public Optional<SelectGraphic> getSelectGraphic() { return models.stream().filter(g -> g instanceof SelectGraphic).map(SelectGraphic.class::cast).findFirst(); } @Override public void setSelectedGraphic(List<Graphic> graphicList) { synchronized (models) { for (Graphic g : models) { g.setSelected(false); } } if (graphicList != null) { for (Graphic g : graphicList) { g.setSelected(true); } } } @Override public void setSelectedAllGraphics() { setSelectedGraphic(getAllGraphics()); } @Override public void deleteSelectedGraphics(Canvas canvas, Boolean warningMessage) { List<Graphic> list = getSelectedGraphics(); if (!list.isEmpty()) { int response = 0; if (warningMessage) { response = JOptionPane.showConfirmDialog(canvas.getJComponent(), String.format(Messages.getString("AbstractLayerModel.del_conf"), list.size()), //$NON-NLS-1$ Messages.getString("AbstractLayerModel.del_graphs"), JOptionPane.YES_NO_OPTION, //$NON-NLS-1$ JOptionPane.WARNING_MESSAGE); } if (Objects.equals(response, 0)) { list.forEach(g -> g.fireRemoveAction()); canvas.getJComponent().repaint(); } } } @Override public void clear() { models.clear(); } @Override public void fireGraphicsSelectionChanged(MeasurableLayer layer) { selectedGraphicsListeners.forEach(gl -> gl.handle(getSelectedGraphics(), layer)); } @Override public void draw(Graphics2D g2d, AffineTransform transform, AffineTransform inverseTransform, Rectangle2D viewClip) { // Get the visible view in real coordinates, note only Sun g2d return consistent clip area with offset Shape area = inverseTransform.createTransformedShape(viewClip == null ? g2d.getClipBounds() : viewClip); Rectangle2D bound = area.getBounds2D(); g2d.translate(0.5, 0.5); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, DefaultView2d.antialiasingOn); models.forEach(g -> applyPaint(g, g2d, transform, bound)); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, DefaultView2d.antialiasingOff); g2d.translate(-0.5, -0.5); } private static void applyPaint(Graphic graphic, Graphics2D g2d, AffineTransform transform, Rectangle2D bounds) { if (graphic.getLayer().getVisible()) { if (bounds != null) { Rectangle repaintBounds = graphic.getRepaintBounds(transform); if (repaintBounds != null && repaintBounds.intersects(bounds)) { graphic.paint(g2d, transform); } else { GraphicLabel graphicLabel = graphic.getGraphicLabel(); if (graphicLabel != null && graphicLabel.getLabels() != null) { Rectangle2D labelBounds = graphicLabel.getBounds(transform); if (labelBounds.intersects(bounds)) { graphic.paintLabel(g2d, transform); } } } } else { // convention is when bounds equals null graphic is repaint graphic.paint(g2d, transform); graphic.paintLabel(g2d, transform); } } } @Override public void addGraphicSelectionListener(GraphicSelectionListener listener) { if (Objects.nonNull(listener) && !selectedGraphicsListeners.contains(listener)) { selectedGraphicsListeners.add(listener); } } @Override public void removeGraphicSelectionListener(GraphicSelectionListener listener) { if (Objects.nonNull(listener)) { selectedGraphicsListeners.remove(listener); } } @Override public List<GraphicModelChangeListener> getChangeListeners() { return modelListeners; } @Override public void addChangeListener(GraphicModelChangeListener listener) { if (Objects.nonNull(listener) && !modelListeners.contains(listener)) { modelListeners.add(listener); } } @Override public void removeChangeListener(GraphicModelChangeListener listener) { Optional.ofNullable(listener).ifPresent(modelListeners::remove); } @Override public void fireChanged() { if (!changeFireingSuspended) { modelListeners.stream().forEach(l -> l.handleModelChanged(this)); } } @Override public Boolean isChangeFireingSuspended() { return changeFireingSuspended; } @Override public void setChangeFireingSuspended(Boolean change) { this.changeFireingSuspended = Optional.ofNullable(change).orElse(Boolean.FALSE); } @Override public void dispose() { modelListeners.clear(); graphicsListeners.clear(); selectedGraphicsListeners.clear(); } @Override public int getLayerCount() { return models.stream().collect(Collectors.groupingBy(getLayer)).size(); } @Override public List<GraphicSelectionListener> getGraphicSelectionListeners() { return selectedGraphicsListeners; } public static Graphic drawFromCurrentGraphic(ViewCanvas<?> canvas, Graphic graphicCreator) { Objects.requireNonNull(canvas); Graphic newGraphic = Optional.ofNullable(graphicCreator).orElse(MeasureToolBar.selectionGraphic); GraphicLayer layer = getOrBuildLayer(canvas, newGraphic.getLayerType()); if (!layer.getVisible() || !(Boolean) canvas.getActionValue(ActionW.DRAWINGS.cmd())) { JOptionPane.showMessageDialog(canvas.getJComponent(), Messages.getString("AbstractLayerModel.msg_not_vis"), //$NON-NLS-1$ Messages.getString("AbstractLayerModel.draw"), //$NON-NLS-1$ JOptionPane.ERROR_MESSAGE); return null; } else { Graphic graph = newGraphic.copy(); if (graph != null) { graph.updateLabel(Boolean.TRUE, canvas); for (PropertyChangeListener listener : canvas.getGraphicManager().getGraphicsListeners()) { graph.addPropertyChangeListener(listener); } graph.setLayer(layer); canvas.getGraphicManager().addGraphic(graph); } return graph; } } public static void addGraphicToModel(ViewCanvas<?> canvas, GraphicLayer layer, Graphic graphic) { GraphicModel gm = canvas.getGraphicManager(); graphic.setLayer(Optional.ofNullable(layer).orElseGet(() -> getOrBuildLayer(canvas, graphic.getLayerType()))); graphic.updateLabel(Boolean.TRUE, canvas); for (PropertyChangeListener listener : canvas.getGraphicManager().getGraphicsListeners()) { graphic.addPropertyChangeListener(listener); } gm.addGraphic(graphic); } public static void addGraphicToModel(ViewCanvas<?> canvas, Graphic graphic) { AbstractGraphicModel.addGraphicToModel(canvas, null, graphic); } public static GraphicLayer getOrBuildLayer(ViewCanvas<?> canvas, LayerType layerType) { return canvas.getGraphicManager().findLayerByType(layerType).orElseGet(() -> new DefaultLayer(layerType)); } private static Predicate<GraphicLayer> isLayerTypeEquals(LayerType type) { // Compare type and if the layer name is null => default layer return layer -> Objects.equals(layer.getType(), type) && layer.getName() == null; } }