View Javadoc
1   package fr.ifremer.dali.ui.swing.util.image;
2   
3   /*
4    * #%L
5    * Dali :: UI
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2014 - 2015 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   *
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.collect.ImmutableList;
27  import fr.ifremer.dali.dto.data.photo.PhotoDTO;
28  import fr.ifremer.quadrige3.core.dao.technical.Assert;
29  import fr.ifremer.quadrige3.core.dao.technical.Images;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  
34  import javax.swing.*;
35  import javax.swing.border.EmptyBorder;
36  import javax.swing.event.ChangeEvent;
37  import javax.swing.event.ChangeListener;
38  import java.awt.*;
39  import java.awt.event.MouseAdapter;
40  import java.awt.event.MouseEvent;
41  import java.awt.event.MouseWheelEvent;
42  import java.awt.image.BufferedImage;
43  import java.io.File;
44  import java.util.List;
45  import java.util.*;
46  
47  import static org.nuiton.i18n.I18n.t;
48  
49  /**
50   * Image Panel component that show one or more PhotoDTO in diaporama or thumbnail mode
51   * <p/>
52   * Created by Ludovic on 30/07/2015.
53   */
54  public class PhotoViewer<P extends PhotoDTO> extends JPanel implements ChangeListener {
55  
56      private final static Log log = LogFactory.getLog(PhotoViewer.class);
57  
58      /**
59       * Constant <code>EVENT_PHOTO_CLICKED="photoClicked"</code>
60       */
61      public static final String EVENT_PHOTO_CLICKED = "photoClicked";
62      /**
63       * Constant <code>PROPERTY_DEFAULT_THUMBNAILS_COLUMNS=6</code>
64       */
65      private static final int PROPERTY_DEFAULT_THUMBNAILS_COLUMNS = 6;
66  
67      private final JPanel imageGrid;
68      private final GridLayout imageGridLayout;
69      private final JScrollPane imageScroll;
70      private final Map<P, BackgroundPanel> photoMap;
71      private Images.ImageType type;
72      private JSlider zoomSlider;
73      private double ratio = BackgroundPanel.NO_SCALE;
74  
75      /**
76       * <p>Constructor for PhotoViewer.</p>
77       */
78      public PhotoViewer() {
79          setLayout(new BorderLayout());
80          setBorder(new EmptyBorder(5, 5, 5, 5));
81          photoMap = new LinkedHashMap<>();
82          imageGridLayout = new GridLayout();
83          imageGrid = new JPanel(imageGridLayout);
84          imageScroll = new JScrollPane(imageGrid);
85          add(imageScroll, BorderLayout.CENTER);
86      }
87  
88      public void setType(Images.ImageType type) {
89          this.type = type;
90  
91      }
92  
93      public void setPhoto(P photo, Images.ImageType type) {
94          setPhotos(photo != null ? ImmutableList.of(photo) : null, type);
95      }
96  
97      /**
98       * <p>setPhotos.</p>
99       *
100      * @param photos a {@link Collection} object.
101      */
102     public void setPhotos(Collection<P> photos, Images.ImageType type) {
103 
104         Assert.notNull(type);
105         setType(type);
106         photoMap.clear();
107 
108         try {
109             if (photos == null) {
110                 return;
111             }
112 
113             for (P photo : photos) {
114 
115                 if (photo == null) {
116                     continue;
117                 }
118 
119                 if (StringUtils.isBlank(photo.getFullPath())) {
120                     log.warn("invalid photo object (no path)");
121                     continue;
122                 }
123 
124                 BufferedImage image = loadPhoto(photo, type);
125                 BackgroundPanel imagePanel = new BackgroundPanel(image, true);
126                 photoMap.put(photo, imagePanel);
127             }
128 
129         } finally {
130             updateImageGrid();
131         }
132     }
133 
134     /**
135      * <p>setSelected.</p>
136      *
137      * @param photo a P object.
138      */
139     public void setSelected(P photo) {
140         setSelected(ImmutableList.of(photo));
141     }
142 
143     /**
144      * <p>setSelected.</p>
145      *
146      * @param photos a {@link Collection} object.
147      */
148     public void setSelected(Collection<P> photos) {
149 
150         // reset selected status
151         photoMap.values().forEach(backgroundPanel -> backgroundPanel.setSelected(false));
152 
153         if (photos == null) return;
154 
155         photos.stream().map(photoMap::get).filter(Objects::nonNull).forEach(backgroundPanel -> {
156             backgroundPanel.setSelected(true);
157             if (!SwingUtilities.isRectangleContainingRectangle(imageScroll.getViewport().getViewRect(), backgroundPanel.getBounds()))
158                 backgroundPanel.scrollRectToVisible(imageScroll.getViewport().getVisibleRect());
159             backgroundPanel.revalidate();
160         });
161     }
162 
163     private BufferedImage loadPhoto(P photo, Images.ImageType type) {
164         BufferedImage image = Images.loadImage(new File(photo.getFullPath()), type, true);
165         if (image == null && type.getMaxSize() != null) {
166             // if photo unavailable, draw a custom image (Mantis #48020)
167             image = new BufferedImage(type.getMaxSize(), type.getMaxSize(), BufferedImage.TYPE_INT_ARGB);
168             Graphics2D graphics = image.createGraphics();
169             // draw light gray background
170             graphics.setColor(new Color(240, 240, 240));
171             graphics.fillRect(0, 0, type.getMaxSize(), type.getMaxSize());
172             // draw texts
173             graphics.setColor(Color.RED.brighter());
174             FontMetrics fm = graphics.getFontMetrics();
175             List<String> texts = new ArrayList<>();
176             texts.addAll(adjustText(fm, t("dali.photo.unavailable"), type.getMaxSize()));
177             texts.addAll(adjustText(fm, t("dali.photo.unavailable.tip"), type.getMaxSize()));
178             float textHeight = fm.getHeight();
179             float textTotalHeight = textHeight * texts.size();
180             // center in canvas
181             for (int i = 0; i < texts.size(); i++) {
182                 String text = texts.get(i);
183                 graphics.drawString(text,
184                         (float) type.getMaxSize() / 2 - ((float) fm.stringWidth(text)) / 2,
185                         (float) type.getMaxSize() / 2 - textTotalHeight / 2 + fm.getAscent() + textHeight * i
186                 );
187             }
188         }
189         return image;
190     }
191 
192     private List<String> adjustText(FontMetrics fontMetrics, String text, int maxWidth) {
193         if (text == null)
194             text = "";
195         List<String> result = new ArrayList<>();
196 
197         // separate each word by space
198         String[] words = text.split("\\s+");
199         if (words.length == 1) {
200             // single word
201             return ImmutableList.of(text);
202         }
203         // build line with the first word
204         StringBuilder line = new StringBuilder(words[0]);
205         int i = 0;
206         while (i < words.length - 1) {
207             // test the length of current line with the next word
208             if (fontMetrics.stringWidth(line + " " + words[i + 1]) < maxWidth) {
209                 if (line.length() > 0) {
210                     // restore the space on each words except the first one
211                     line.append(" ");
212                 }
213                 // append the word to the current line
214                 line.append(words[++i]);
215             } else {
216                 // this line is complete, append it to result and create a new one
217                 result.add(line.toString());
218                 line = new StringBuilder();
219                 if (fontMetrics.stringWidth(words[i + 1]) > maxWidth) {
220                     // if the next word is too large, append it to result directly (it will be cropped)
221                     result.add(words[++i]);
222                 }
223             }
224         }
225         if (line.length() > 0) {
226             // append the remaining line
227             result.add(line.toString());
228         }
229         return result;
230     }
231 
232     private void updateImageGrid() {
233 
234         imageGrid.removeAll();
235 
236         if (type == Images.ImageType.THUMBNAIL) {
237 
238             imageGridLayout.setColumns(PROPERTY_DEFAULT_THUMBNAILS_COLUMNS);
239             imageGridLayout.setRows(0);
240 
241         } else {
242 
243             imageGridLayout.setColumns(1);
244             imageGridLayout.setRows(1);
245 
246         }
247 
248         photoMap.values().forEach(backgroundPanel -> {
249             initBackgroundPanel(backgroundPanel);
250             imageGrid.add(backgroundPanel);
251         });
252 
253         revalidate();
254     }
255 
256     private void initBackgroundPanel(BackgroundPanel image) {
257 
258         // Add mouse listener
259         image.addMouseListener(mouseAdapter);
260         image.addMouseMotionListener(mouseAdapter);
261         image.addMouseWheelListener(mouseAdapter);
262         image.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
263 
264         if (type == Images.ImageType.BASE) {
265             createZoomSlider();
266         } else if (zoomSlider != null) {
267             remove(zoomSlider);
268         }
269     }
270 
271     private final MouseAdapter mouseAdapter = new MouseAdapter() {
272 
273         private Point origin;
274 
275         @Override
276         public void mouseClicked(MouseEvent e) {
277             // fire event on click
278             if (e.getComponent() instanceof BackgroundPanel) {
279                 for (P photo : photoMap.keySet()) {
280                     if (e.getComponent() == photoMap.get(photo)) {
281                         firePropertyChange(EVENT_PHOTO_CLICKED, null, photo);
282                         return;
283                     }
284                 }
285             }
286 
287         }
288 
289         @Override
290         public void mousePressed(MouseEvent e) {
291             origin = new Point(e.getPoint());
292             e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
293         }
294 
295         @Override
296         public void mouseReleased(MouseEvent e) {
297             e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
298         }
299 
300         @Override
301         public void mouseWheelMoved(MouseWheelEvent e) {
302             if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL)
303                 zoom(e.getWheelRotation());
304         }
305 
306         @Override
307         public void mouseDragged(MouseEvent e) {
308             if (origin != null) {
309                 JComponent component = (JComponent) e.getSource();
310                 int deltaX = origin.x - e.getX();
311                 int deltaY = origin.y - e.getY();
312 
313                 Rectangle view = imageScroll.getViewport().getViewRect();
314                 view.x += deltaX;
315                 view.y += deltaY;
316 
317                 component.scrollRectToVisible(view);
318             }
319         }
320 
321     };
322 
323     /**
324      * {@inheritDoc}
325      */
326     @Override
327     public void stateChanged(ChangeEvent e) {
328         if (e.getSource() != zoomSlider) return;
329         if (zoomSlider.getValueIsAdjusting()) return;
330         int zoom = ((JSlider) e.getSource()).getValue();
331         ratio = zoom / 100.0;
332 
333         Arrays.stream(imageGrid.getComponents()).filter(component -> component instanceof BackgroundPanel)
334                 .map(component -> (BackgroundPanel) component)
335                 .forEach(backgroundPanel -> backgroundPanel.setRatio(ratio));
336 
337         revalidate();
338     }
339 
340     private void createZoomSlider() {
341         zoomSlider = new JSlider(JSlider.HORIZONTAL, 10, 100, 100);
342         zoomSlider.setMajorTickSpacing(10);
343         zoomSlider.setPaintTicks(true);
344         zoomSlider.setPaintLabels(true);
345         zoomSlider.setSnapToTicks(true);
346         zoomSlider.addChangeListener(this);
347         add(zoomSlider, BorderLayout.PAGE_END);
348     }
349 
350     private void zoom(int amount) {
351         if (zoomSlider == null || amount == 0) return;
352 
353         zoomSlider.setValue(zoomSlider.getValue() + (10 * (amount < 0 ? 1 : -1)));
354 
355     }
356 }