001package javax.swing.text.html;
002
003import gnu.javax.swing.text.html.ImageViewIconFactory;
004import gnu.javax.swing.text.html.css.Length;
005
006import java.awt.Graphics;
007import java.awt.Image;
008import java.awt.MediaTracker;
009import java.awt.Rectangle;
010import java.awt.Shape;
011import java.awt.Toolkit;
012import java.awt.image.ImageObserver;
013import java.net.MalformedURLException;
014import java.net.URL;
015
016import javax.swing.Icon;
017import javax.swing.SwingUtilities;
018import javax.swing.text.AbstractDocument;
019import javax.swing.text.AttributeSet;
020import javax.swing.text.BadLocationException;
021import javax.swing.text.Document;
022import javax.swing.text.Element;
023import javax.swing.text.View;
024import javax.swing.text.Position.Bias;
025import javax.swing.text.html.HTML.Attribute;
026
027/**
028 * A view, representing a single image, represented by the HTML IMG tag.
029 *
030 * @author Audrius Meskauskas (AudriusA@Bioinformatics.org)
031 */
032public class ImageView extends View
033{
034  /**
035   * Tracks image loading state and performs the necessary layout updates.
036   */
037  class Observer
038    implements ImageObserver
039  {
040
041    public boolean imageUpdate(Image image, int flags, int x, int y, int width, int height)
042    {
043      boolean widthChanged = false;
044      if ((flags & ImageObserver.WIDTH) != 0 && spans[X_AXIS] == null)
045        widthChanged = true;
046      boolean heightChanged = false;
047      if ((flags & ImageObserver.HEIGHT) != 0 && spans[Y_AXIS] == null)
048        heightChanged = true;
049      if (widthChanged || heightChanged)
050        safePreferenceChanged(ImageView.this, widthChanged, heightChanged);
051      boolean ret = (flags & ALLBITS) != 0;
052      return ret;
053    }
054
055  }
056
057  /**
058   * True if the image loads synchronuosly (on demand). By default, the image
059   * loads asynchronuosly.
060   */
061  boolean loadOnDemand;
062
063  /**
064   * The image icon, wrapping the image,
065   */
066  Image image;
067
068  /**
069   * The image state.
070   */
071  byte imageState = MediaTracker.LOADING;
072
073  /**
074   * True when the image needs re-loading, false otherwise.
075   */
076  private boolean reloadImage;
077
078  /**
079   * True when the image properties need re-loading, false otherwise.
080   */
081  private boolean reloadProperties;
082
083  /**
084   * True when the width is set as CSS/HTML attribute.
085   */
086  private boolean haveWidth;
087
088  /**
089   * True when the height is set as CSS/HTML attribute.
090   */
091  private boolean haveHeight;
092
093  /**
094   * True when the image is currently loading.
095   */
096  private boolean loading;
097
098  /**
099   * The current width of the image.
100   */
101  private int width;
102
103  /**
104   * The current height of the image.
105   */
106  private int height;
107
108  /**
109   * Our ImageObserver for tracking the loading state.
110   */
111  private ImageObserver observer;
112
113  /**
114   * The CSS width and height.
115   *
116   * Package private to avoid synthetic accessor methods.
117   */
118  Length[] spans;
119
120  /**
121   * The cached attributes.
122   */
123  private AttributeSet attributes;
124
125  /**
126   * Creates the image view that represents the given element.
127   *
128   * @param element the element, represented by this image view.
129   */
130  public ImageView(Element element)
131  {
132    super(element);
133    spans = new Length[2];
134    observer = new Observer();
135    reloadProperties = true;
136    reloadImage = true;
137    loadOnDemand = false;
138  }
139
140  /**
141   * Load or reload the image. This method initiates the image reloading. After
142   * the image is ready, the repaint event will be scheduled. The current image,
143   * if it already exists, will be discarded.
144   */
145  private void reloadImage()
146  {
147    loading = true;
148    reloadImage = false;
149    haveWidth = false;
150    haveHeight = false;
151    image = null;
152    width = 0;
153    height = 0;
154    try
155      {
156        loadImage();
157        updateSize();
158      }
159    finally
160      {
161        loading = false;
162      }
163  }
164
165  /**
166   * Get the image alignment. This method works handling standart alignment
167   * attributes in the HTML IMG tag (align = top bottom middle left right).
168   * Depending from the parameter, either horizontal or vertical alingment
169   * information is returned.
170   *
171   * @param axis -
172   *          either X_AXIS or Y_AXIS
173   */
174  public float getAlignment(int axis)
175  {
176    AttributeSet attrs = getAttributes();
177    Object al = attrs.getAttribute(Attribute.ALIGN);
178
179    // Default is top left aligned.
180    if (al == null)
181      return 0.0f;
182
183    String align = al.toString();
184
185    if (axis == View.X_AXIS)
186      {
187        if (align.equals("middle"))
188          return 0.5f;
189        else if (align.equals("left"))
190          return 0.0f;
191        else if (align.equals("right"))
192          return 1.0f;
193        else
194          return 0.0f;
195      }
196    else if (axis == View.Y_AXIS)
197      {
198        if (align.equals("middle"))
199          return 0.5f;
200        else if (align.equals("top"))
201          return 0.0f;
202        else if (align.equals("bottom"))
203          return 1.0f;
204        else
205          return 0.0f;
206      }
207    else
208      throw new IllegalArgumentException("axis " + axis);
209  }
210
211  /**
212   * Get the text that should be shown as the image replacement and also as the
213   * image tool tip text. The method returns the value of the attribute, having
214   * the name {@link Attribute#ALT}. If there is no such attribute, the image
215   * name from the url is returned. If the URL is not available, the empty
216   * string is returned.
217   */
218  public String getAltText()
219  {
220    Object rt = getAttributes().getAttribute(Attribute.ALT);
221    if (rt != null)
222      return rt.toString();
223    else
224      {
225        URL u = getImageURL();
226        if (u == null)
227          return "";
228        else
229          return u.getFile();
230      }
231  }
232
233  /**
234   * Returns the combination of the document and the style sheet attributes.
235   */
236  public AttributeSet getAttributes()
237  {
238    if (attributes == null)
239      attributes = getStyleSheet().getViewAttributes(this);
240    return attributes;
241  }
242
243  /**
244   * Get the image to render. May return null if the image is not yet loaded.
245   */
246  public Image getImage()
247  {
248    updateState();
249    return image;
250  }
251
252  /**
253   * Get the URL location of the image to render. If this method returns null,
254   * the "no image" icon is rendered instead. By defaul, url must be present as
255   * the "src" property of the IMG tag. If it is missing, null is returned and
256   * the "no image" icon is rendered.
257   *
258   * @return the URL location of the image to render.
259   */
260  public URL getImageURL()
261  {
262    Element el = getElement();
263    String src = (String) el.getAttributes().getAttribute(Attribute.SRC);
264    URL url = null;
265    if (src != null)
266      {
267        URL base = ((HTMLDocument) getDocument()).getBase();
268        try
269          {
270            url = new URL(base, src);
271          }
272        catch (MalformedURLException ex)
273          {
274            // Return null.
275          }
276      }
277    return url;
278  }
279
280  /**
281   * Get the icon that should be displayed while the image is loading and hence
282   * not yet available.
283   *
284   * @return an icon, showing a non broken sheet of paper with image.
285   */
286  public Icon getLoadingImageIcon()
287  {
288    return ImageViewIconFactory.getLoadingImageIcon();
289  }
290
291  /**
292   * Get the image loading strategy.
293   *
294   * @return false (default) if the image is loaded when the view is
295   *         constructed, true if the image is only loaded on demand when
296   *         rendering.
297   */
298  public boolean getLoadsSynchronously()
299  {
300    return loadOnDemand;
301  }
302
303  /**
304   * Get the icon that should be displayed when the image is not available.
305   *
306   * @return an icon, showing a broken sheet of paper with image.
307   */
308  public Icon getNoImageIcon()
309  {
310    return ImageViewIconFactory.getNoImageIcon();
311  }
312
313  /**
314   * Get the preferred span of the image along the axis. The image size is first
315   * requested to the attributes {@link Attribute#WIDTH} and
316   * {@link Attribute#HEIGHT}. If they are missing, and the image is already
317   * loaded, the image size is returned. If there are no attributes, and the
318   * image is not loaded, zero is returned.
319   *
320   * @param axis -
321   *          either X_AXIS or Y_AXIS
322   * @return either width of height of the image, depending on the axis.
323   */
324  public float getPreferredSpan(int axis)
325  {
326    Image image = getImage();
327
328    if (axis == View.X_AXIS)
329      {
330        if (spans[axis] != null)
331          return spans[axis].getValue();
332        else if (image != null)
333          return image.getWidth(getContainer());
334        else
335          return getNoImageIcon().getIconWidth();
336      }
337    else if (axis == View.Y_AXIS)
338      {
339        if (spans[axis] != null)
340          return spans[axis].getValue();
341        else if (image != null)
342          return image.getHeight(getContainer());
343        else
344          return getNoImageIcon().getIconHeight();
345      }
346    else
347      throw new IllegalArgumentException("axis " + axis);
348  }
349
350  /**
351   * Get the associated style sheet from the document.
352   *
353   * @return the associated style sheet.
354   */
355  protected StyleSheet getStyleSheet()
356  {
357    HTMLDocument doc = (HTMLDocument) getDocument();
358    return doc.getStyleSheet();
359  }
360
361  /**
362   * Get the tool tip text. This is overridden to return the value of the
363   * {@link #getAltText()}. The parameters are ignored.
364   *
365   * @return that is returned by getAltText().
366   */
367  public String getToolTipText(float x, float y, Shape shape)
368  {
369    return getAltText();
370  }
371
372  /**
373   * Paints the image or one of the two image state icons. The image is resized
374   * to the shape bounds. If there is no image available, the alternative text
375   * is displayed besides the image state icon.
376   *
377   * @param g
378   *          the Graphics, used for painting.
379   * @param bounds
380   *          the bounds of the region where the image or replacing icon must be
381   *          painted.
382   */
383  public void paint(Graphics g, Shape bounds)
384  {
385    updateState();
386    Rectangle r = bounds instanceof Rectangle ? (Rectangle) bounds
387                                              : bounds.getBounds();
388    Image image = getImage();
389    if (image != null)
390      {
391        g.drawImage(image, r.x, r.y, r.width, r.height, observer);
392      }
393    else
394      {
395        Icon icon = getNoImageIcon();
396        if (icon != null)
397          icon.paintIcon(getContainer(), g, r.x, r.y);
398      }
399  }
400
401  /**
402   * Set if the image should be loaded only when needed (synchronuosly). By
403   * default, the image loads asynchronuosly. If the image is not yet ready, the
404   * icon, returned by the {@link #getLoadingImageIcon()}, is displayed.
405   */
406  public void setLoadsSynchronously(boolean load_on_demand)
407  {
408    loadOnDemand = load_on_demand;
409  }
410
411  /**
412   * Update all cached properties from the attribute set, returned by the
413   * {@link #getAttributes}.
414   */
415  protected void setPropertiesFromAttributes()
416  {
417    AttributeSet atts = getAttributes();
418    StyleSheet ss = getStyleSheet();
419    float emBase = ss.getEMBase(atts);
420    float exBase = ss.getEXBase(atts);
421    spans[X_AXIS] = (Length) atts.getAttribute(CSS.Attribute.WIDTH);
422    if (spans[X_AXIS] != null)
423      {
424        spans[X_AXIS].setFontBases(emBase, exBase);
425      }
426    spans[Y_AXIS] = (Length) atts.getAttribute(CSS.Attribute.HEIGHT);
427    if (spans[Y_AXIS] != null)
428      {
429        spans[Y_AXIS].setFontBases(emBase, exBase);
430      }
431  }
432
433  /**
434   * Maps the picture co-ordinates into the image position in the model. As the
435   * image is not divideable, this is currently implemented always to return the
436   * start offset.
437   */
438  public int viewToModel(float x, float y, Shape shape, Bias[] bias)
439  {
440    return getStartOffset();
441  }
442
443  /**
444   * This is currently implemented always to return the area of the image view,
445   * as the image is not divideable by character positions.
446   *
447   * @param pos character position
448   * @param area of the image view
449   * @param bias bias
450   *
451   * @return the shape, where the given character position should be mapped.
452   */
453  public Shape modelToView(int pos, Shape area, Bias bias)
454      throws BadLocationException
455  {
456    return area;
457  }
458
459  /**
460   * Starts loading the image asynchronuosly. If the image must be loaded
461   * synchronuosly instead, the {@link #setLoadsSynchronously} must be
462   * called before calling this method. The passed parameters are not used.
463   */
464  public void setSize(float width, float height)
465  {
466    updateState();
467    // TODO: Implement this when we have an alt view for the alt=... attribute.
468  }
469
470  /**
471   * This makes sure that the image and properties have been loaded.
472   */
473  private void updateState()
474  {
475    if (reloadImage)
476      reloadImage();
477    if (reloadProperties)
478      setPropertiesFromAttributes();
479  }
480
481  /**
482   * Actually loads the image.
483   */
484  private void loadImage()
485  {
486    URL src = getImageURL();
487    Image newImage = null;
488    if (src != null)
489      {
490        // Call getImage(URL) to allow the toolkit caching of that image URL.
491        Toolkit tk = Toolkit.getDefaultToolkit();
492        newImage = tk.getImage(src);
493        tk.prepareImage(newImage, -1, -1, observer);
494        if (newImage != null && getLoadsSynchronously())
495          {
496            // Load image synchronously.
497            MediaTracker tracker = new MediaTracker(getContainer());
498            tracker.addImage(newImage, 0);
499            try
500              {
501                tracker.waitForID(0);
502              }
503            catch (InterruptedException ex)
504              {
505                Thread.interrupted();
506              }
507
508          }
509      }
510    image = newImage;
511  }
512
513  /**
514   * Updates the size parameters of the image.
515   */
516  private void updateSize()
517  {
518    int newW = 0;
519    int newH = 0;
520    Image newIm = getImage();
521    if (newIm != null)
522      {
523        // Fetch width.
524        Length l = spans[X_AXIS];
525        if (l != null)
526          {
527            newW = (int) l.getValue();
528            haveWidth = true;
529          }
530        else
531          {
532            newW = newIm.getWidth(observer);
533          }
534        // Fetch height.
535        l = spans[Y_AXIS];
536        if (l != null)
537          {
538            newH = (int) l.getValue();
539            haveHeight = true;
540          }
541        else
542          {
543            newW = newIm.getWidth(observer);
544          }
545        // Go and trigger loading.
546        Toolkit tk = Toolkit.getDefaultToolkit();
547        if (haveWidth || haveHeight)
548          tk.prepareImage(newIm, width, height, observer);
549        else
550          tk.prepareImage(newIm, -1, -1, observer);
551      }
552  }
553
554  /**
555   * Calls preferenceChanged from the event dispatch thread and within
556   * a read lock to protect us from threading issues.
557   *
558   * @param v the view
559   * @param width true when the width changed
560   * @param height true when the height changed
561   */
562  void safePreferenceChanged(final View v, final boolean width,
563                             final boolean height)
564  {
565    if (SwingUtilities.isEventDispatchThread())
566      {
567        Document doc = getDocument();
568        if (doc instanceof AbstractDocument)
569          ((AbstractDocument) doc).readLock();
570        try
571          {
572            preferenceChanged(v, width, height);
573          }
574        finally
575          {
576            if (doc instanceof AbstractDocument)
577              ((AbstractDocument) doc).readUnlock();
578          }
579      }
580    else
581      {
582        SwingUtilities.invokeLater(new Runnable()
583        {
584          public void run()
585          {
586            safePreferenceChanged(v, width, height);
587          }
588        });
589      }
590  }
591}