Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: org/apache/batik/bridge/CursorManager.java


1   /*
2   
3      Copyright 2002-2004  The Apache Software Foundation 
4   
5      Licensed under the Apache License, Version 2.0 (the "License");
6      you may not use this file except in compliance with the License.
7      You may obtain a copy of the License at
8   
9          http://www.apache.org/licenses/LICENSE-2.0
10  
11     Unless required by applicable law or agreed to in writing, software
12     distributed under the License is distributed on an "AS IS" BASIS,
13     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14     See the License for the specific language governing permissions and
15     limitations under the License.
16  
17   */
18  package org.apache.batik.bridge;
19  
20  import java.awt.Cursor;
21  import java.awt.Dimension;
22  import java.awt.Image;
23  import java.awt.Point;
24  import java.awt.Rectangle;
25  import java.awt.Toolkit;
26  import java.awt.geom.AffineTransform;
27  import java.awt.geom.Point2D;
28  import java.awt.image.BufferedImage;
29  import java.awt.image.ColorModel;
30  import java.awt.image.Raster;
31  import java.awt.image.RenderedImage;
32  import java.awt.image.SampleModel;
33  import java.awt.image.WritableRaster;
34  import java.util.Hashtable;
35  import java.util.Map;
36  
37  import org.apache.batik.css.engine.SVGCSSEngine;
38  import org.apache.batik.css.engine.value.Value;
39  import org.apache.batik.dom.svg.XMLBaseSupport;
40  import org.apache.batik.dom.util.XLinkSupport;
41  import org.apache.batik.ext.awt.image.PadMode;
42  import org.apache.batik.ext.awt.image.renderable.AffineRable8Bit;
43  import org.apache.batik.ext.awt.image.renderable.Filter;
44  import org.apache.batik.ext.awt.image.renderable.PadRable8Bit;
45  import org.apache.batik.ext.awt.image.spi.ImageTagRegistry;
46  import org.apache.batik.gvt.GraphicsNode;
47  import org.apache.batik.util.ParsedURL;
48  import org.apache.batik.util.SVGConstants;
49  import org.apache.batik.util.SoftReferenceCache;
50  import org.w3c.dom.Element;
51  import org.w3c.dom.Node;
52  import org.w3c.dom.css.CSSPrimitiveValue;
53  import org.w3c.dom.css.CSSValue;
54  import org.w3c.dom.svg.SVGDocument;
55  import org.w3c.dom.svg.SVGPreserveAspectRatio;
56  
57  
58  /**
59   * The CursorManager class is a helper class which preloads the cursors 
60   * corresponding to the SVG built in cursors.
61   *
62   * @author <a href="mailto:vincent.hardy@sun.com">Vincent Hardy</a>
63   * @version $Id: CursorManager.java,v 1.17 2005/03/27 08:58:30 cam Exp $
64   */
65  public class CursorManager implements SVGConstants, ErrorConstants {
66      /**
67       * Maps SVG Cursor Values to Java Cursors
68       */
69      protected static Map cursorMap;
70  
71      /**
72       * Default cursor when value is not found
73       */
74      public static final Cursor DEFAULT_CURSOR 
75          = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
76  
77      /**
78       * Cursor used over anchors
79       */
80      public static final Cursor ANCHOR_CURSOR
81          = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
82  
83      /**
84       * Cursor used over text
85       */
86      public static final Cursor TEXT_CURSOR
87          = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
88  
89      /**
90       * Default preferred cursor size, used for SVG images
91       */
92      public static final int DEFAULT_PREFERRED_WIDTH = 32;
93      public static final int DEFAULT_PREFERRED_HEIGHT = 32;
94  
95      /**
96       * Static initialization of the cursorMap
97       */
98      static {
99          cursorMap = new Hashtable();
100         cursorMap.put(SVG_CROSSHAIR_VALUE,
101                       Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
102         cursorMap.put(SVG_DEFAULT_VALUE,
103                       Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
104         cursorMap.put(SVG_POINTER_VALUE,
105                       Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
106         cursorMap.put(SVG_MOVE_VALUE,
107                       Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
108         cursorMap.put(SVG_E_RESIZE_VALUE,
109                       Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
110         cursorMap.put(SVG_NE_RESIZE_VALUE,
111                       Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR));
112         cursorMap.put(SVG_NW_RESIZE_VALUE,
113                       Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR));
114         cursorMap.put(SVG_N_RESIZE_VALUE,
115                       Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
116         cursorMap.put(SVG_SE_RESIZE_VALUE,
117                       Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
118         cursorMap.put(SVG_SW_RESIZE_VALUE,
119                       Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR));
120         cursorMap.put(SVG_S_RESIZE_VALUE,
121                       Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR));
122         cursorMap.put(SVG_W_RESIZE_VALUE,
123                       Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
124         cursorMap.put(SVG_TEXT_VALUE,
125                       Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
126         cursorMap.put(SVG_WAIT_VALUE,
127                       Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
128         cursorMap.put(SVG_HELP_VALUE, 
129                       Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));  
130         
131     }
132 
133     /**
134      * BridgeContext associated with this CursorManager
135      */
136     protected BridgeContext ctx;
137 
138     /**
139      * Cache used to hold references to cursors
140      */
141     protected CursorCache cursorCache = new CursorCache();
142 
143     /**
144      * Creates a new CursorManager object.
145      *
146      * @param ctx the BridgeContext associated to this CursorManager
147      */
148     public CursorManager(BridgeContext ctx) {
149         this.ctx = ctx;
150     }
151 
152     /**
153      * Returns a Cursor object for a given cursor value. This initial
154      * implementation does not handle user-defined cursors, so it
155      * always uses the cursor at the end of the list
156      */
157     public static Cursor getPredefinedCursor(String cursorName){
158         return (Cursor)cursorMap.get(cursorName);
159     }
160 
161 
162 
163     /**
164      * Returns the Cursor corresponding to the input element's cursor property
165      *
166      * @param e the element on which the cursor property is set
167      */
168     public Cursor convertCursor(Element e) {
169         Value cursorValue = CSSUtilities.getComputedStyle
170             (e, SVGCSSEngine.CURSOR_INDEX);
171 
172         String cursorStr = SVGConstants.SVG_AUTO_VALUE;
173 
174         if (cursorValue != null) {
175             if (cursorValue.getCssValueType() == CSSValue.CSS_PRIMITIVE_VALUE
176                 &&
177                 cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
178                 // Single Value : should be one of the predefined cursors or 
179                 // 'inherit'
180                 cursorStr = cursorValue.getStringValue();
181                 return convertBuiltInCursor(e, cursorStr);
182             } else if (cursorValue.getCssValueType() == 
183                        CSSValue.CSS_VALUE_LIST) {
184                 int nValues = cursorValue.getLength();
185                 if (nValues == 1) {
186                     cursorValue = cursorValue.item(0);
187                     if (cursorValue.getPrimitiveType() == 
188                         CSSPrimitiveValue.CSS_IDENT) {
189                         cursorStr = cursorValue.getStringValue();
190                         return convertBuiltInCursor(e, cursorStr);
191                     }
192                 } else if (nValues > 1) {
193                     //
194                     // Look for the first cursor url we can handle.
195                     // That would be a reference to a <cursor> element.
196                     //
197                     return convertSVGCursor(e, cursorValue);
198                 }
199             }
200         } 
201         
202         return convertBuiltInCursor(e, cursorStr);
203     }
204     
205     public Cursor convertBuiltInCursor(Element e, String cursorStr) {
206         Cursor cursor = null;
207 
208         // The CSS engine guarantees an non null, non empty string
209         // as the computed value for cursor. Therefore, the following
210         // test is safe.
211         if (cursorStr.charAt(0) == 'a') { 
212             //
213             // Handle 'auto' value.
214             //
215             // - <a> The following sets the cursor for <a> element
216             // enclosing text nodes. Setting the proper cursor (i.e.,
217             // depending on the children's 'cursor' property, is
218             // handled in the SVGAElementBridge so as to avoid going
219             // up the tree on mouseover events (looking for an anchor
220             // ancestor.
221             //
222             // - <image> The following does not change the cursor if
223             // the element's cursor property is set to
224             // 'auto'. Otherwise, it takes precedence over any child
225             // (in case of SVG content) cursor setting.  This means
226             // that for images referencing SVG content, a cursor
227             // property set to 'auto' on the <image> element will not
228             // override the cursor settings inside the SVG image. Any
229             // other cursor property will take precedence.
230             //
231             // - <use> Same behavior as for <image> except that the
232             // behavior is controlled from the <use> element bridge
233             // (SVGUseElementBridge).
234             //
235             // - <text>, <tref> and <tspan> : a cursor value of auto
236             // will cause the cursor to be set to a text cursor. Note
237             // that text content with an 'auto' cursor and descendant
238             // of an anchor will have its cursor set to the anchor
239             // cursor through the SVGAElementBridge.
240             //
241             String nameSpaceURI = e.getNamespaceURI();
242             if (SVGConstants.SVG_NAMESPACE_URI.equals(nameSpaceURI)) {
243                 String tag = e.getLocalName();
244                 if (SVGConstants.SVG_A_TAG.equals(tag)) {
245                     cursor = CursorManager.ANCHOR_CURSOR;
246                 } else if (SVGConstants.SVG_TEXT_TAG.equals(tag) ||
247                            SVGConstants.SVG_TSPAN_TAG.equals(tag) ||
248                            SVGConstants.SVG_TREF_TAG.equals(tag) ) {
249                     cursor = CursorManager.TEXT_CURSOR;
250                 } else if (SVGConstants.SVG_IMAGE_TAG.equals(tag)) {
251                     // Do not change the cursor 
252                     return null;
253                 } else {
254                     cursor = CursorManager.DEFAULT_CURSOR;
255                 }
256             } else {
257                 cursor = CursorManager.DEFAULT_CURSOR;
258             }
259         } else {
260             // Specific, logical cursor
261             cursor = CursorManager.getPredefinedCursor(cursorStr);
262         }
263         
264         return cursor;
265     }
266 
267        
268     /**
269      * Returns a cursor for the given value list. Note that the 
270      * code assumes that the input value has at least two entries.
271      * So the caller should check that before calling the method.
272      * For example, CSSUtilities.convertCursor performs that check.
273      */
274     public Cursor convertSVGCursor(Element e, Value l) {
275         int nValues = l.getLength();
276         Element cursorElement = null;
277         for (int i=0; i<nValues-1; i++) {
278             Value cursorValue = l.item(i);
279             if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_URI) {
280                 String uri = cursorValue.getStringValue();
281                 
282                 // If the uri does not resolve to a cursor element,
283                 // then, this is not a type of cursor uri we can handle:
284                 // go to the next or default to logical cursor
285                 try {
286                     cursorElement = ctx.getReferencedElement(e, uri);
287                 } catch (BridgeException be) {
288                     // Be only silent if this is a case where the target
289                     // could not be found. Do not catch other errors (e.g,
290                     // malformed URIs)
291                     if (!ERR_URI_BAD_TARGET.equals(be.getCode())) {
292                         throw be;
293                     }
294                 }
295                 
296                 if (cursorElement != null) {
297                     // We go an element, check it is of type cursor
298                     String cursorNS = cursorElement.getNamespaceURI();
299                     if (SVGConstants.SVG_NAMESPACE_URI.equals(cursorNS) &&
300                         SVGConstants.SVG_CURSOR_TAG.equals
301                         (cursorElement.getLocalName())) {
302                         Cursor c = convertSVGCursorElement(cursorElement);
303                         if (c != null) { 
304                             return c;
305                         }
306                     }
307                 }
308             }
309         }
310         
311         // If we got to that point, it means that no cursorElement
312         // produced a valid cursor, i.e., either a format we support
313         // or a valid referenced image (no broken image).
314         // Fallback on the built in cursor property.
315         Value cursorValue = l.item(nValues-1);
316         String cursorStr = SVGConstants.SVG_AUTO_VALUE;
317         if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
318             cursorStr = cursorValue.getStringValue();
319         }
320           
321         return convertBuiltInCursor(e, cursorStr);
322     }
323 
324     /**
325      * Returns a cursor for a given element
326      */
327     public Cursor convertSVGCursorElement(Element cursorElement) {
328         // One of the cursor url resolved to a <cursor> element
329         // Try to handle its image.
330         String uriStr = XLinkSupport.getXLinkHref(cursorElement);
331         if (uriStr.length() == 0) {
332             throw new BridgeException(cursorElement, ERR_ATTRIBUTE_MISSING,
333                                       new Object[] {"xlink:href"});
334         }
335 
336         String baseURI = XMLBaseSupport.getCascadedXMLBase(cursorElement);
337         ParsedURL purl;
338         if (baseURI == null) {
339             purl = new ParsedURL(uriStr);
340         } else {
341             purl = new ParsedURL(baseURI, uriStr);
342         }
343 
344         //
345         // Convert the cursor's hot spot
346         //
347         UnitProcessor.Context uctx 
348             = UnitProcessor.createContext(ctx, cursorElement);
349 
350         String s = cursorElement.getAttributeNS(null, SVG_X_ATTRIBUTE);
351         float x = 0;
352         if (s.length() != 0) {
353             x = UnitProcessor.svgHorizontalCoordinateToUserSpace
354                 (s, SVG_X_ATTRIBUTE, uctx);
355         }
356 
357         s = cursorElement.getAttributeNS(null, SVG_Y_ATTRIBUTE);
358         float y = 0;
359         if (s.length() != 0) {
360             y = UnitProcessor.svgVerticalCoordinateToUserSpace
361                 (s, SVG_Y_ATTRIBUTE, uctx);
362         }
363 
364         CursorDescriptor desc = new CursorDescriptor(purl, x, y);
365 
366         //
367         // Check if there is a cursor in the cache for this url
368         //
369         Cursor cachedCursor = cursorCache.getCursor(desc);
370 
371         if (cachedCursor != null) {
372             return cachedCursor;
373         } 
374         
375         //
376         // Load image into Filter f and transform hotSpot to 
377         // cursor space.
378         //
379         Point2D.Float hotSpot = new Point2D.Float(x, y);
380         Filter f = cursorHrefToFilter(cursorElement, 
381                                       purl, 
382                                       hotSpot);
383         if (f == null) {
384             cursorCache.clearCursor(desc);
385             return null;
386         }
387             
388         // The returned Filter is guaranteed to create a 
389         // default rendering of the desired size
390         Rectangle cursorSize = f.getBounds2D().getBounds();
391         RenderedImage ri = f.createScaledRendering(cursorSize.width,
392                                                    cursorSize.height,
393                                                    null);
394         Image img = null;
395 
396         if (ri instanceof Image) {
397             img = (Image)ri;
398         } else {
399             img = renderedImageToImage(ri);
400         }
401 
402         // Make sure the not spot does not fall out of the cursor area. If it
403         // does, then clamp the coordinates to the image space.
404         hotSpot.x = hotSpot.x < 0 ? 0 : hotSpot.x;
405         hotSpot.y = hotSpot.y < 0 ? 0 : hotSpot.y;
406         hotSpot.x = hotSpot.x > (cursorSize.width-1) ? cursorSize.width - 1 : hotSpot.x;
407         hotSpot.y = hotSpot.y > (cursorSize.height-1) ? cursorSize.height - 1: hotSpot.y;
408 
409         //
410         // The cursor image is now into 'img'
411         //
412         Cursor c = Toolkit.getDefaultToolkit()
413             .createCustomCursor(img, 
414                                 new Point(Math.round(hotSpot.x),
415                                           Math.round(hotSpot.y)),
416                                 purl.toString());
417 
418         cursorCache.putCursor(desc, c);
419         return c;        
420     }
421 
422     /**
423      * Converts the input ParsedURL into a Filter and transforms the 
424      * input hotSpot point (in image space) to cursor space
425      */
426     protected Filter cursorHrefToFilter(Element cursorElement, 
427                                         ParsedURL purl,
428                                         Point2D hotSpot) {
429         
430         AffineRable8Bit f = null;
431         String uriStr = purl.toString();
432         Dimension cursorSize = null;
433 
434         // Try to load as an SVG Document
435         DocumentLoader loader = ctx.getDocumentLoader();
436         SVGDocument svgDoc = (SVGDocument)cursorElement.getOwnerDocument();
437         URIResolver resolver = new URIResolver(svgDoc, loader);
438         try {
439             Element rootElement = null;
440             Node n = resolver.getNode(uriStr, cursorElement);
441             if (n.getNodeType() == Node.DOCUMENT_NODE) {
442                 SVGDocument doc = (SVGDocument)n;
443                 // FIXX: really should be subCtx here.
444                 ctx.initializeDocument(doc); 
445                 rootElement = doc.getRootElement();
446             } else {
447                 throw new BridgeException 
448                     (cursorElement, ERR_URI_IMAGE_INVALID,
449                      new Object[] {uriStr});
450             }
451             GraphicsNode node = ctx.getGVTBuilder().build(ctx, rootElement);
452 
453             //
454             // The cursorSize define the viewport into which the 
455             // cursor is displayed. That viewport is platform 
456             // dependant and is not defined by the SVG content.
457             //
458             float width  = DEFAULT_PREFERRED_WIDTH;
459             float height = DEFAULT_PREFERRED_HEIGHT;
460             UnitProcessor.Context uctx 
461                 = UnitProcessor.createContext(ctx, rootElement);
462 
463             String s = rootElement.getAttribute(SVG_WIDTH_ATTRIBUTE);
464             if (s.length() != 0) {
465                 width = UnitProcessor.svgHorizontalLengthToUserSpace
466                 (s, SVG_WIDTH_ATTRIBUTE, uctx);
467             }
468             
469             s = rootElement.getAttribute(SVG_HEIGHT_ATTRIBUTE);
470             if (s.length() != 0) {
471                 height = UnitProcessor.svgVerticalLengthToUserSpace
472                     (s, SVG_HEIGHT_ATTRIBUTE, uctx);
473             }
474 
475             cursorSize 
476                 = Toolkit.getDefaultToolkit().getBestCursorSize
477                 (Math.round(width), Math.round(height));
478             
479             // Handle the viewBox transform
480             AffineTransform at 
481                 = ViewBox.getPreserveAspectRatioTransform(rootElement,
482                                                           cursorSize.width,
483                                                           cursorSize.height);
484             Filter filter = node.getGraphicsNodeRable(true);
485             f = new AffineRable8Bit(filter, at);
486         } catch (BridgeException ex) {
487             throw ex;
488         } catch (SecurityException ex) {
489             throw new BridgeException(cursorElement, ERR_URI_UNSECURE,
490                                       new Object[] {uriStr});
491         } catch (Exception ex) {
492             /* Nothing to do */ 
493         }
494 
495 
496         // If f is null, it means that we are not dealing with
497         // an SVG image. Try as a raster image.
498         if (f == null) {
499             ImageTagRegistry reg = ImageTagRegistry.getRegistry();
500             Filter filter = reg.readURL(purl);
501             if (filter == null) {
502                 return null;
503             }
504 
505             // Check if we got a broken image
506             if (filter.getProperty
507                 (SVGBrokenLinkProvider.SVG_BROKEN_LINK_DOCUMENT_PROPERTY) != null) {
508                 return null;
509             } 
510             
511             Rectangle preferredSize = filter.getBounds2D().getBounds();
512             cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize
513                 (preferredSize.width, preferredSize.height);
514             
515             if (preferredSize != null && preferredSize.width >0
516                 && preferredSize.height > 0 ) {
517                 AffineTransform at = new AffineTransform();
518                 if (preferredSize.width > cursorSize.width 
519                     ||
520                     preferredSize.height > cursorSize.height) {
521                     at = ViewBox.getPreserveAspectRatioTransform
522                         (new float[] {0, 0, preferredSize.width, preferredSize.height},
523                          SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMIN,
524                          true,
525                          cursorSize.width,
526                          cursorSize.height);
527                 } 
528                 f = new AffineRable8Bit(filter, at);
529             } else {
530                 // Invalid Size
531                 return null;
532             }
533         }
534 
535 
536         //
537         // Transform the hot spot from image space to cursor space
538         //
539         AffineTransform at = f.getAffine();
540         at.transform(hotSpot, hotSpot);
541 
542         //
543         // In all cases, clip to the cursor boundaries
544         //
545         Rectangle cursorViewport 
546             = new Rectangle(0, 0, cursorSize.width, cursorSize.height);
547 
548         PadRable8Bit cursorImage 
549             = new PadRable8Bit(f, cursorViewport,
550                                PadMode.ZERO_PAD);
551 
552         return cursorImage;
553             
554     }
555 
556 
557     /**
558      * Implementation helper: converts a RenderedImage to an Image
559      */
560     protected Image renderedImageToImage(RenderedImage ri) {
561         int x = ri.getMinX();
562         int y = ri.getMinY();
563         SampleModel sm = ri.getSampleModel();
564         ColorModel cm = ri.getColorModel();
565         WritableRaster wr = Raster.createWritableRaster(sm, new Point(x,y));
566         ri.copyData(wr);
567 
568         return new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);
569     }
570 
571     /**
572      * Simple inner class which holds the information describing
573      * a cursor, i.e., the image it points to and the hot spot point
574      * coordinates.
575      */
576     static class CursorDescriptor {
577         ParsedURL purl;
578         float x;
579         float y;
580         String desc;
581 
582         public CursorDescriptor(ParsedURL purl,
583                                 float x, float y) {
584             if (purl == null) {
585                 throw new IllegalArgumentException();
586             }
587 
588             this.purl = purl;
589             this.x = x;
590             this.y = y;
591 
592             // Desc is used for hascode as well as for toString()
593             this.desc = this.getClass().getName() + 
594                 "\n\t:[" + this.purl + "]\n\t:[" + x + "]:[" + y + "]";
595         }
596 
597         public boolean equals(Object obj) {
598             if (obj == null 
599                 ||
600                 !(obj instanceof CursorDescriptor)) {
601                 return false;
602             }
603 
604             CursorDescriptor desc = (CursorDescriptor)obj;
605             boolean isEqual =  
606                 this.purl.equals(desc.purl)
607                  &&
608                  this.x == desc.x
609                  &&
610                  this.y == desc.y;
611                  
612             return isEqual;
613         }
614 
615         public String toString() {
616             return this.desc;
617         }
618 
619         public int hashCode() {
620             return desc.hashCode();
621         }
622     }
623 
624     /**
625      * Simple extension of the SoftReferenceCache that 
626      * offers typed interface (Kind of needed as SoftReferenceCache
627      * mostly has protected methods).
628      */
629     static class CursorCache extends SoftReferenceCache {
630         public CursorCache() {
631         }
632 
633         public Cursor getCursor(CursorDescriptor desc) {
634             return (Cursor)requestImpl(desc);
635         }
636 
637         public void putCursor(CursorDescriptor desc, 
638                               Cursor cursor) {
639             putImpl(desc, cursor);
640         }
641 
642         public void clearCursor(CursorDescriptor desc) {
643             clearImpl(desc);
644         }
645     }
646 
647 
648 
649 }