001    package javax.swing.text.html;
002    
003    import gnu.javax.swing.text.html.ImageViewIconFactory;
004    import gnu.javax.swing.text.html.css.Length;
005    
006    import java.awt.Graphics;
007    import java.awt.Image;
008    import java.awt.MediaTracker;
009    import java.awt.Rectangle;
010    import java.awt.Shape;
011    import java.awt.Toolkit;
012    import java.awt.image.ImageObserver;
013    import java.net.MalformedURLException;
014    import java.net.URL;
015    
016    import javax.swing.Icon;
017    import javax.swing.SwingUtilities;
018    import javax.swing.text.AbstractDocument;
019    import javax.swing.text.AttributeSet;
020    import javax.swing.text.BadLocationException;
021    import javax.swing.text.Document;
022    import javax.swing.text.Element;
023    import javax.swing.text.View;
024    import javax.swing.text.Position.Bias;
025    import 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     */
032    public 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    }