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    AttributeSet attrs = getAttributes();
327    
328    Image image = getImage();
329
330    if (axis == View.X_AXIS)
331      {
332        if (spans[axis] != null)
333          return spans[axis].getValue();
334        else if (image != null)
335          return image.getWidth(getContainer());
336        else
337          return getNoImageIcon().getIconWidth();
338      }
339    else if (axis == View.Y_AXIS)
340      {
341        if (spans[axis] != null)
342          return spans[axis].getValue();
343        else if (image != null)
344          return image.getHeight(getContainer());
345        else
346          return getNoImageIcon().getIconHeight();
347      }
348    else
349      throw new IllegalArgumentException("axis " + axis);
350  }
351  
352  /**
353   * Get the associated style sheet from the document.
354   * 
355   * @return the associated style sheet.
356   */
357  protected StyleSheet getStyleSheet()
358  {
359    HTMLDocument doc = (HTMLDocument) getDocument();
360    return doc.getStyleSheet();
361  }
362
363  /**
364   * Get the tool tip text. This is overridden to return the value of the
365   * {@link #getAltText()}. The parameters are ignored.
366   * 
367   * @return that is returned by getAltText().
368   */
369  public String getToolTipText(float x, float y, Shape shape)
370  {
371    return getAltText();
372  }
373
374  /**
375   * Paints the image or one of the two image state icons. The image is resized
376   * to the shape bounds. If there is no image available, the alternative text
377   * is displayed besides the image state icon.
378   * 
379   * @param g
380   *          the Graphics, used for painting.
381   * @param bounds
382   *          the bounds of the region where the image or replacing icon must be
383   *          painted.
384   */
385  public void paint(Graphics g, Shape bounds)
386  {
387    updateState();
388    Rectangle r = bounds instanceof Rectangle ? (Rectangle) bounds
389                                              : bounds.getBounds();
390    Image image = getImage();
391    if (image != null)
392      {
393        g.drawImage(image, r.x, r.y, r.width, r.height, observer);
394      }
395    else
396      {
397        Icon icon = getNoImageIcon();
398        if (icon != null)
399          icon.paintIcon(getContainer(), g, r.x, r.y);
400      }
401  }
402
403  /**
404   * Set if the image should be loaded only when needed (synchronuosly). By
405   * default, the image loads asynchronuosly. If the image is not yet ready, the
406   * icon, returned by the {@link #getLoadingImageIcon()}, is displayed.
407   */
408  public void setLoadsSynchronously(boolean load_on_demand)
409  {
410    loadOnDemand = load_on_demand;
411  }
412 
413  /**
414   * Update all cached properties from the attribute set, returned by the
415   * {@link #getAttributes}.
416   */
417  protected void setPropertiesFromAttributes()
418  {
419    AttributeSet atts = getAttributes();
420    StyleSheet ss = getStyleSheet();
421    float emBase = ss.getEMBase(atts);
422    float exBase = ss.getEXBase(atts);
423    spans[X_AXIS] = (Length) atts.getAttribute(CSS.Attribute.WIDTH);
424    if (spans[X_AXIS] != null)
425      {
426        spans[X_AXIS].setFontBases(emBase, exBase);
427      }
428    spans[Y_AXIS] = (Length) atts.getAttribute(CSS.Attribute.HEIGHT);
429    if (spans[Y_AXIS] != null)
430      {
431        spans[Y_AXIS].setFontBases(emBase, exBase);
432      }
433  }
434  
435  /**
436   * Maps the picture co-ordinates into the image position in the model. As the
437   * image is not divideable, this is currently implemented always to return the
438   * start offset.
439   */
440  public int viewToModel(float x, float y, Shape shape, Bias[] bias)
441  {
442    return getStartOffset();
443  }
444  
445  /**
446   * This is currently implemented always to return the area of the image view,
447   * as the image is not divideable by character positions.
448   * 
449   * @param pos character position
450   * @param area of the image view
451   * @param bias bias
452   * 
453   * @return the shape, where the given character position should be mapped.
454   */
455  public Shape modelToView(int pos, Shape area, Bias bias)
456      throws BadLocationException
457  {
458    return area;
459  }
460  
461  /**
462   * Starts loading the image asynchronuosly. If the image must be loaded
463   * synchronuosly instead, the {@link #setLoadsSynchronously} must be
464   * called before calling this method. The passed parameters are not used.
465   */
466  public void setSize(float width, float height)
467  {
468    updateState();
469    // TODO: Implement this when we have an alt view for the alt=... attribute.
470  }  
471
472  /**
473   * This makes sure that the image and properties have been loaded.
474   */
475  private void updateState()
476  {
477    if (reloadImage)
478      reloadImage();
479    if (reloadProperties)
480      setPropertiesFromAttributes();
481  }
482
483  /**
484   * Actually loads the image.
485   */
486  private void loadImage()
487  {
488    URL src = getImageURL();
489    Image newImage = null;
490    if (src != null)
491      {
492        // Call getImage(URL) to allow the toolkit caching of that image URL.
493        Toolkit tk = Toolkit.getDefaultToolkit();
494        newImage = tk.getImage(src);
495        tk.prepareImage(newImage, -1, -1, observer);
496        if (newImage != null && getLoadsSynchronously())
497          {
498            // Load image synchronously.
499            MediaTracker tracker = new MediaTracker(getContainer());
500            tracker.addImage(newImage, 0);
501            try
502              {
503                tracker.waitForID(0);
504              }
505            catch (InterruptedException ex)
506              {
507                Thread.interrupted();
508              }
509            
510          }
511      }
512    image = newImage;
513  }
514
515  /**
516   * Updates the size parameters of the image.
517   */
518  private void updateSize()
519  {
520    int newW = 0;
521    int newH = 0;
522    Image newIm = getImage();
523    if (newIm != null)
524      {
525        AttributeSet atts = getAttributes();
526        // Fetch width.
527        Length l = spans[X_AXIS];
528        if (l != null)
529          {
530            newW = (int) l.getValue();
531            haveWidth = true;
532          }
533        else
534          {
535            newW = newIm.getWidth(observer);
536          }
537        // Fetch height.
538        l = spans[Y_AXIS];
539        if (l != null)
540          {
541            newH = (int) l.getValue();
542            haveHeight = true;
543          }
544        else
545          {
546            newW = newIm.getWidth(observer);
547          }
548        // Go and trigger loading.
549        Toolkit tk = Toolkit.getDefaultToolkit();
550        if (haveWidth || haveHeight)
551          tk.prepareImage(newIm, width, height, observer);
552        else
553          tk.prepareImage(newIm, -1, -1, observer);
554      }
555  }
556
557  /**
558   * Calls preferenceChanged from the event dispatch thread and within
559   * a read lock to protect us from threading issues.
560   *
561   * @param v the view
562   * @param width true when the width changed
563   * @param height true when the height changed
564   */
565  void safePreferenceChanged(final View v, final boolean width,
566                             final boolean height)
567  {
568    if (SwingUtilities.isEventDispatchThread())
569      {
570        Document doc = getDocument();
571        if (doc instanceof AbstractDocument)
572          ((AbstractDocument) doc).readLock();
573        try
574          {
575            preferenceChanged(v, width, height);
576          }
577        finally
578          {
579            if (doc instanceof AbstractDocument)
580              ((AbstractDocument) doc).readUnlock();
581          }
582      }
583    else
584      {
585        SwingUtilities.invokeLater(new Runnable()
586        {
587          public void run()
588          {
589            safePreferenceChanged(v, width, height);
590          }
591        });
592      }
593  }
594}