Added: sis/ip-review/rev/01188/ZoomPane.xhtml URL: http://svn.apache.org/viewvc/sis/ip-review/rev/01188/ZoomPane.xhtml?rev=1884563&view=auto ============================================================================== --- sis/ip-review/rev/01188/ZoomPane.xhtml (added) +++ sis/ip-review/rev/01188/ZoomPane.xhtml Thu Dec 17 17:18:06 2020 @@ -0,0 +1,1796 @@ + + + + + ZoomPane changes for revisions 1094:1188 + + + +
+

ZoomPane changes for revisions 1094:1188

+ +

Translation of comments from French to English. + The LINDA UP TO HERE comment suggests that translations of previous lines (not shown in this commit) + were also done by someone else than the committer. This commit contains also some code reformatting, + but the reformatting part does not apply to Apache SIS (code in SIS has been reformatted again).

+ +

The translations are not works that we can port to Apache SIS as-is. + However the original work – the sentences in French – can be ported to Apache SIS without problems. + In the code committed in Apache repository, we retranslated some sentences from the original French sentences. + Some other sentences have only been reworded. I did that when the sentence was similar to what I would have written + if I was redoing the translation from French anyway, but with sentences written in a more "javadoc-like" way and with + some terms replaced by more standard words used in Java (e.g. "redefine" → "override", "display" → "show", etc.). + The sentences that I did not changed are sentences that I would have written in approximately the same way anyway + if I was redoing the translation from French.

+ +

As of December 2020, this code is not committed in the main Apache SIS branch. + Instead we create a separated branch called "visual test" for now, with no intent to bring it back to main branch. + The reason is that we do not plan to provide Swing application in Apache SIS; instead we are developing a JavaFX application. + This Swing component is ported only as a debugging tool.

+ +

In this process I have learn a new English word: "whilst".

+ +
svn diff --extensions "--unified --ignore-space-change --ignore-all-space --ignore-eol-style" -r1094:1188 https://svn.osgeo.org/geotools/trunk/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/ZoomPane.java
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Revision 1094Revision 1188
* by the user through the scrollbars will be translated by calls to
+* {@link #transform}.</p>
+*
+* @version 1.0
+* @author Martin Desruisseaux
+*/
* by the user through the scrollbars will be translated by calls to
+* {@link #transform}.</p>
+*
+* $Id: ZoomPane.java,v 1.4 2002/07/22 09:24:18 jmacgill Exp $
+* @version 1.0
+* @author Martin Desruisseaux
+*/
 * @version 1.0
+ * @author Martin Desruisseaux
+ */
+private final class Listeners extends MouseAdapter implements MouseWheelListener, ComponentListener, Serializable
+{
+    public void mouseWheelMoved (final MouseWheelEvent event) {ZoomPane.this.mouseWheelMoved (event);}
+    public void mousePressed    (final MouseEvent      event) {ZoomPane.this.mayShowPopupMenu(event);}
 * @version 1.0
+ * @author Martin Desruisseaux
+ */
+private final class Listeners extends MouseAdapter implements MouseWheelListener,
+                                                              ComponentListener, Serializable
+{
+    public void mouseWheelMoved (final MouseWheelEvent event) {ZoomPane.this.mouseWheelMoved (event);}
+    public void mousePressed    (final MouseEvent      event) {ZoomPane.this.mayShowPopupMenu(event);}
        }
+        double m=amount;
+        if (button || (event.getModifiers() & ActionEvent.SHIFT_MASK)!=0) {
+            if ((actionType & UNIFORM_SCALE)!=0) m = (m>=1) ? 2.0 : 0.5;
+            else                                 m*= ENHANCEMENT_FACTOR;
+        }
+        transform(actionType & type, m, point);
+    }
+};
        }
+        double m=amount;
+        if (button || (event.getModifiers() & ActionEvent.SHIFT_MASK)!=0) {
+            if ((actionType & UNIFORM_SCALE) != 0) {
+                m = (m >= 1) ? 2.0 : 0.5;
+        }
+            else {
+                m *= ENHANCEMENT_FACTOR;
+            }
+        }
+        transform(actionType & type, m, point);
+    }
+};
    unexpectedException("reset", exception);
+    return;
+}
+if (yAxisUpward) zoom.setToScale(+1, -1);
+else             zoom.setToIdentity();
+final AffineTransform transform=setVisibleArea(preferredArea,
+                                               zoomableBounds);
+change.concatenate(zoom);
    unexpectedException("reset", exception);
+    return;
+}
+if (yAxisUpward) {
+    zoom.setToScale(+1, -1);
+}
+else {
+    zoom.setToIdentity();
+}
+final AffineTransform transform=setVisibleArea(preferredArea,
+                                               zoomableBounds);
+change.concatenate(zoom);
    visible=XAffineTransform.inverseTransform(zoom, zoomableBounds, null);
+} catch (NoninvertibleTransformException exception) {
+    unexpectedException("getVisibleArea", exception);
+    visible=new Rectangle2D.Double(zoomableBounds.getCenterX(), zoomableBounds.getCenterY(), 0, 0);
+}
+visibleArea.setRect(visible);
+return visible;
    visible=XAffineTransform.inverseTransform(zoom, zoomableBounds, null);
+} catch (NoninvertibleTransformException exception) {
+    unexpectedException("getVisibleArea", exception);
+    visible = new Rectangle2D.Double(zoomableBounds.getCenterX(),
+                                     zoomableBounds.getCenterY(), 0, 0);
+}
+visibleArea.setRect(visible);
+return visible;
 * @param  logicalBounds Logical coordinates of the region to be displayed.
+ * @throws IllegalArgumentException if <code>source</code> is empty.
+ */
+public final void setVisibleArea(final Rectangle2D logicalBounds) throws IllegalArgumentException {
+    log("setVisibleArea", logicalBounds);
+    transform(setVisibleArea(logicalBounds, getZoomableBounds()));
+}
 * @param  logicalBounds Logical coordinates of the region to be displayed.
+ * @throws IllegalArgumentException if <code>source</code> is empty.
+ */
+public void setVisibleArea(final Rectangle2D logicalBounds)
+       throws IllegalArgumentException {
+    log("setVisibleArea", logicalBounds);
+    transform(setVisibleArea(logicalBounds, getZoomableBounds()));
+}
 * @return Change to apply to the affine transform {@link #zoom}.
+ * @throws IllegalArgumentException if <code>source</code> is empty.
+ */
+private AffineTransform setVisibleArea(Rectangle2D source, Rectangle2D dest) throws IllegalArgumentException {
+    /*
+     * Verifies the validity of the rectangle <code>source</code>. An
+     * invalid rectangle will be rejected. However, we will be more
 * @return Change to apply to the affine transform {@link #zoom}.
+ * @throws IllegalArgumentException if <code>source</code> is empty.
+ */
+private AffineTransform setVisibleArea(Rectangle2D source, Rectangle2D dest)
+                                       throws IllegalArgumentException {
+    /*
+     * Verifies the validity of the rectangle <code>source</code>. An
+     * invalid rectangle will be rejected. However, we will be more
if ((type & UNIFORM_SCALE) == UNIFORM_SCALE) {
+    if (fillPanel)
+    {
+             if (sy*sourceWidth  > destWidth ) sx=sy;
+        else if (sx*sourceHeight > destHeight) sy=sx;
+    }
+    else
+    {
+             if (sy*sourceWidth  < destWidth ) sx=sy;
+        else if (sx*sourceHeight < destHeight) sy=sx;
+    }
+}
+final AffineTransform change=AffineTransform.getTranslateInstance(
+                 (type & TRANSLATE_X)!=0 ? dest.getCenterX()    : 0,
+                 (type & TRANSLATE_Y)!=0 ? dest.getCenterY()    : 0);
if ((type & UNIFORM_SCALE) == UNIFORM_SCALE) {
+    if (fillPanel)
+    {
+             if (sy * sourceWidth  > destWidth ) {
+                 sx = sy;
+    }
+        else if (sx * sourceHeight > destHeight) {
+            sy = sx;
+        }
+    }
+    else
+    {
+             if (sy * sourceWidth  < destWidth ) {
+                 sx=sy;
+    }
+        else if (sx * sourceHeight < destHeight) {
+            sy=sx;
+}
+    }
+}
+final AffineTransform change=AffineTransform.getTranslateInstance(
+                 (type & TRANSLATE_X)!=0 ? dest.getCenterX()    : 0,
+                 (type & TRANSLATE_Y)!=0 ? dest.getCenterY()    : 0);
}
+
+/**
+ * LINDA UP TO HERE
+ Change the {@linkplain #zoom} by applying and affine transform. The
+ * <code>change</code> transform must express a change if logical units,
+ * for example a translation in meters. This method is conceptually similar
+ * to the following code:
+ *
+ * <pre>
+ * {@link #zoom}.{@link AffineTransform#concatenate(AffineTransform) concatenate}(change);
}
+
+/**
+ * Changes the {@linkplain #zoom} by applying an affine transform. The
+ * <code>change</code> transform must express a change in logical units,
+ * for example, a translation in metres. This method is conceptually
+ * similar to the following code:
+ *
+ * <pre>
+ * {@link #zoom}.{@link AffineTransform#concatenate(AffineTransform) concatenate}(change);
 *
+ * @param  change The zoom change, as an affine transform in logical
+ *         coordinates. If <code>change</code> is the identity transform,
+ *         then this method do nothing and listeners are not notified.
+ */
+public void transform(final AffineTransform change) {
+    if (!change.isIdentity()) {
 *
+ * @param  change The zoom change, as an affine transform in logical
+ *         coordinates. If <code>change</code> is the identity transform,
+ *         then this method does nothing and listeners are not notified.
+ */
+public void transform(final AffineTransform change) {
+    if (!change.isIdentity()) {
}
+
+/**
+ * Effectue un zoom, une translation ou une rotation sur le contenu de
+ * <code>ZoomPane</code>. Le type d'opération à effectuer dépend de
+ * l'argument <code>operation</code>:
+ *
+ * <ul>
+ *   <li>{@link #TRANSLATE_X} effectue une translation le long de l'axe des
+ *       <var>x</var>. L'argument <code>amount</code> spécifie la
+ *       transformation à effectuer en nombre de pixels. Une valeur négative
+ *       déplace vers la gauche tandis qu'une valeur positive déplace vers
+ *       la droite.</li>
+ *   <li>{@link #TRANSLATE_Y} effectue une translation le long de l'axe des
+ *       <var>y</var>. L'argument <code>amount</code> spécifie la
+ *       transformation à effectuer en nombre de pixels. Une valeur négative
+ *       déplace vers le haut tandis qu'une valeur positive déplace vers le
+ *       bas.</li>
+ *   <li>{@link #UNIFORM_SCALE} effectue un zoom. L'argument
+ *       <code>zoom</code> spécifie le zoom à effectuer. Une valeur
+ *       supérieure à 1 effectuera un zoom avant, tandis qu'une valeur
+ *       comprise entre 0 et 1 effectuera un zoom arrière.</li>
+ *   <li>{@link #ROTATE} effectue une rotation. L'argument <code>zoom</code>
+ *       spécifie l'angle de rotation en radians.</li>
+ *   <li>{@link #RESET} Redéfinit le zoom à une échelle, rotation et
+ *       translation par défaut. Cette opération aura pour effet de faire
+ *       apparaître la totalité ou quasi-totalité du contenu de
+ *       <code>ZoomPane</code>.</li>
+ *   <li>{@link #DEFAULT_ZOOM} Effectue un zoom par défaut, proche du zoom
+ *       maximal, qui fait voir les détails du contenu de
+ *       <code>ZoomPane</code> mais sans les grossir exégarément.</li>
+ * </ul>
+ *
+ * @param  operation Type d'opération à effectuer.
+ * @param  amount Translation en pixels ({@link #TRANSLATE_X} et
+ *         {@link #TRANSLATE_Y}), facteur d'échelle ({@link #SCALE_X} et
+ *         {@link #SCALE_Y}) ou angle de rotation en radians
+ *         ({@link #ROTATE}). Dans les autres cas, cet argument est ignoré
+ *         et peut être {@link Double#NaN}.
+ * @param  center Centre du zoom ({@link #SCALE_X} et {@link #SCALE_Y}) ou
+ *         de la rotation ({@link #ROTATE}), en coordonnées pixels. La
+ *         valeur <code>null</code> désigne une valeur par défaut, le plus
+ *         souvent le centre de la fenêtre.
+ * @throws UnsupportedOperationException si l'argument
+ *         <code>operation</code> n'est pas reconnu.
+ */
+private void transform(final int operation,
+                       final double amount,
}
+
+/**
+ * Carries out a zoom, a translation or a rotation on the contents of
+ * <code>ZoomPane</code>. The type of operation to carry out depends on the
+ * <code>operation</code> argument:
+ *
+ * <ul>
+ *   <li>{@link #TRANSLATE_X} carries out a translation along the
+ *       <var>x</var> axis. The <code>amount</code> argument specifies the
+ *       transformation to perform in number of pixels. A negative value
+ *       moves to the left whilst a positive value moves to the right.</li>
+ *   <li>{@link #TRANSLATE_Y} carries out a translation along the
+ *       <var>y</var> axis. The <code>amount</code> argument specifies the
+ *       transformation to perform in number of pixels. A negative value
+ *       moves upwards whilst a positive value moves downwards.</li>
+ *   <li>{@link #UNIFORM_SCALE} carries out a zoom. The <code>zoom</code>
+ *       argument specifies the type of zoom to perform. A value greater
+ *       than 1 will perform a zoom in whilst a value between 0 and 1 will
+ *       perform a zoom out.</li>
+ *   <li>{@link #ROTATE} carries out a rotation. The <code>zoom</code>
+ *       argument specifies the rotation angle in radians.</li>
+ *   <li>{@link #RESET} Redefines the zoom to a default scale, rotation
+ *       and translation. This operation displays all, or almost all, the
+ *       contents of <code>ZoomPane</code>.</li>
+ *   <li>{@link #DEFAULT_ZOOM} Carries out a default zoom, close to the
+ *       maximum zoom, which shows the details of the contents of
+ *       <code>ZoomPane</code> but without enlarging them too much.</li>
+ * </ul>
+ *
+ * @param  operation Type of operation to perform.
+ * @param  amount ({@link #TRANSLATE_X} and
+ *         {@link #TRANSLATE_Y}) translation in pixels, ({@link #SCALE_X}
+ *         and {@link #SCALE_Y}) scale factor or ({@link #ROTATE}) rotation
+ *         angle in radians.
+ *         In other cases, this argument is ignored and can be
+ *         {@link Double#NaN}.
+ * @param  center Zoom centre ({@link #SCALE_X} and {@link #SCALE_Y}) or
+ *         rotation centre ({@link #ROTATE}), in pixel coordinates. The
+ *         value <code>null</code> indicates a default value, more often
+ *         not the centre of the window.
+ * @throws UnsupportedOperationException if the <code>operation</code>
+ *         <code>operation</code> argument isn't recognized.
+ */
+private void transform(final int operation,
+                       final double amount,
                     ((operation & TRANSLATE_Y)!=0) ? amount : 0);
+} else {
+    /*
+     * Obtient les coordonnées (en pixels)
+     * du centre de rotation ou du zoom.
+     */
+    final double centerX;
+    final double centerY;
                     ((operation & TRANSLATE_Y)!=0) ? amount : 0);
+} else {
+    /*
+     * Obtains the coordinates (in pixels) of the rotation or
+     * zoom centre.
+     */
+    final double centerX;
+    final double centerY;
        return;
+    }
+    /*
+     * On accepte les largeurs et hauteurs de 0. Si toutefois le
+     * rectangle n'est pas valide (largeur ou hauteur négatif),
+     * alors on terminera cette méthode sans rien faire. Aucun
+     * zoom n'aura été effectué.
+     */
+}
+if ((operation & (ROTATE))!=0) {
        return;
+    }
+    /*
+     * Zero lengths and widths are accepted.  If, however, the
+     * rectangle isn't valid (negative length or width) then the
+     * method will end without doing anything. No zoom will be
+     * performed.
+     */
+}
+if ((operation & (ROTATE))!=0) {
}
+
+/**
+ * Ajoute un objet à la liste des objets intéressés
+ * à être informés des changements de zoom.
+ */
+public void addZoomChangeListener(final ZoomChangeListener listener) {
+    listenerList.add(ZoomChangeListener.class, listener);
}
+
+/**
+ * Adds an object to the list of objects interested in being notified
+ * about zoom changes.
+ */
+public void addZoomChangeListener(final ZoomChangeListener listener) {
+    listenerList.add(ZoomChangeListener.class, listener);
}
+
+/**
+ * Retire un objet de la liste des objets intéressés
+ * à être informés des changements de zoom.
+ */
+public void removeZoomChangeListener(final ZoomChangeListener listener) {
+    listenerList.remove(ZoomChangeListener.class, listener);
}
+
+/**
+ * Removes an object from the list of objects interested in being notified
+ * about zoom changes.
+ */
+public void removeZoomChangeListener(final ZoomChangeListener listener) {
+    listenerList.remove(ZoomChangeListener.class, listener);
}
+
+/**
+ * Ajoute un objet à la liste des objets intéressés
+ * à être informés des événements de la souris.
+ */
+public void addMouseListener(final MouseListener listener) {
+    super.removeMouseListener(mouseSelectionTracker);
}
+
+/**
+ * Adds an object to the list of objects interested in being notified
+ * about mouse events.
+ */
+public void addMouseListener(final MouseListener listener) {
+    super.removeMouseListener(mouseSelectionTracker);
}
+
+/**
+ * Signale qu'un changement du zoom vient d'être effectué. Chaque objets
+ * enregistrés par la méthode {@link #addZoomChangeListener} sera prévenu
+ * du changement aussitôt que possible.
+ *
+ * @param change Transformation affine qui représente le changement dans le
+ *               zoom. Soit <code>oldZoom</code> et <code>newZoom</code> les
+ *               transformations affines de l'ancien et du nouveau zoom
+ *               respectivement. Alors la relation
+ *
+ * <code>newZoom=oldZoom.{@link AffineTransform#concatenate concatenate}(change)</code>
+ *
+ *               doit être respectée (aux erreurs d'arrondissements près).
+ *               <strong>Notez que cette méthode peut modifier
+ *               <code>change</code></strong> pour combiner en une seule
+ *               transformation plusieurs appels consécutifs de
+ *               <code>fireZoomChanged</code>.
+ */
+protected void fireZoomChanged(final AffineTransform change) {
+    visibleArea.setRect(getVisibleArea());
}
+
+/**
+ * Signals that a zoom change has taken place. Every object registered by
+ * the {@link #addZoomChangeListener} method will be notified of the change
+ * as soon as possible.
+ *
+ * @param change Affine transform which represents the change in the zoom.
+ *               That is <code>oldZoom</code> and <code>newZoom</code> are
+ *               the affine transforms of the old and new zoom respectively.
+ *               Therefore, the relation
+ * <code>newZoom=oldZoom.{@link AffineTransform#concatenate concatenate}(change)</code>
+ *               must be respected (to within rounding errors).
+ *               <strong>Note: This method can modify
+ *               <code>change</code></strong> to combine several
+ *               consecutive calls of
+ *               <code>fireZoomChanged</code> in a single transformation.
+ */
+protected void fireZoomChanged(final AffineTransform change) {
+    visibleArea.setRect(getVisibleArea());
}
+
+/**
+ * Préviens les classes dérivées que le zoom a changé. Contrairement à la
+ * méthode {@link #fireZoomChanged} protégée, cette méthode privée ne
+ * modifie aucun champ interne et n'essaye pas d'appeller d'autres méthodes
+ * de <code>ZoomPane</code> comme {@link #getVisibleArea}. On évite ainsi
+ * une boucle sans fin lorsque cette méthode est appelée par {@link #reset}.
+ */
+private void fireZoomChanged0(final AffineTransform change) {
+    /*
+     * Note: il faut lancer l'événement même si la transformation
+     *       est la matrice identité, car certaine classe utilise
+     *       ce truc pour mettre à jour les barres de défilements.
+     */
+    if (change==null) {
+        throw new NullPointerException();
}
+
+/**
+ * Notifies derived classes that the zoom has changed. Unlike the
+ * protected {@link #fireZoomChanged} method, this private method doesn't
+ * modify any internal field and doesn't attempt to call other
+ * <code>ZoomPane> methods such as {@link #getVisibleArea}. An infinite
+ * loop is thereby avoided as this method is called by {@link #reset}.
+ */
+private void fireZoomChanged0(final AffineTransform change) {
+    /*
+     * Note: the event must be fired even if the transformation
+     *       is the identity matrix, because certain classes use
+     *       this to update scrollbars.
+     */
+    if (change==null) {
+        throw new NullPointerException();
}
+
+/**
+ * Méthode appelée automatiquement après que l'utilisateur ait sélectionnée
+ * une région à l'aide de la souris. L'implémentation par défaut zoom la
+ * région <code>area</code> sélectionnée. Les classes dérivées peuvent
+ * redéfinir cette méthode pour entreprendre une autre action.
+ *
+ * @param area Région sélectionnée par l'utilisateur, en coordonnées
+ *        logiques.
+ */
+protected void mouseSelectionPerformed(final Shape area) {
+    final Rectangle2D rect=(area instanceof Rectangle2D) ? (Rectangle2D) area : area.getBounds2D();
}
+
+/**
+ * Method called automatically after the user selects an area with the
+ * mouse. The default implementation zooms to the selected
+ * <code>area</code>. Derived classes can redefine this method in order
+ * to carry out another action.
+ *
+ * @param area Area selected by the user, in logical coordinates.
+ */
+protected void mouseSelectionPerformed(final Shape area) {
+    final Rectangle2D rect=(area instanceof Rectangle2D) ? (Rectangle2D) area : area.getBounds2D();
}
+
+/**
+ * Retourne la forme géométrique à utiliser pour délimiter une région.
+ * Cette forme est généralement un rectangle mais pourrait aussi être
+ * une ellipse, une flèche ou d'autres formes encore. Les coordonnées
+ * de la forme retournée ne seront pas prises en compte. En fait, ces
+ * coordonnées seront régulièrement écrasées.  Seule compte la classe
+ * de la forme retournée (par exemple {@link java.awt.geom.Ellipse2D}
+ * vs {@link java.awt.geom.Rectangle2D}) et ses paramètres non-reliés
+ * à sa position (par exemple l'arrondissement des coins d'un rectangle
+ * {@link java.awt.geom.RoundRectangle2D}).
+ *
+ * La forme retournée sera généralement d'une classe dérivée de
+ * {@link RectangularShape}, mais peut aussi être de la classe
+ * {@link Line2D}. <strong>Tout autre classe risque de lancer une
+ * {@link ClassCastException} au moment de l'exécution</strong>.
+ *
+ * L'implémentation par défaut retourne toujours un objet
+ * {@link java.awt.geom.Rectangle2D}.
+ *
+ * @param  event Coordonnées logiques de la souris au moment ou le bouton a
+ *         été enfoncé. Cette information peut être utilisée par les classes
+ *         dérivées qui voudraient tenir compte de la position de la souris
+ *         avant de choisir une forme géométrique.
+ * @return Forme de la classe {link RectangularShape} ou {link Line2D}, ou
+ *         <code>null</code> pour indiquer qu'on ne veut pas faire de
+ *         sélection avec la souris.
+ */
+protected Shape getMouseSelectionShape(final Point2D point) {
+    return new Rectangle2D.Float();
}
+
+/**
+ * Returns the geometric shape to be used to delimitate an area.
+ * This shape is generally a rectangle but could also be an ellipse,
+ * an arrow or another shape. The coordinates of the returned shape
+ * won't be taken into account. In fact, these coordinates will often
+ * be destroyed. The only things which count are the class of the
+ * returned shape (e.g. {@link java.awt.geom.Ellipse2D} vs
+ * {@link java.awt.geom.Rectangle2D}) and any of its parameters not
+ * related to its position (e.g. corner rounding in a rectangle
+ * {@link java.awt.geom.RoundRectangle2D}).
+ *
+ * The returned shape will generally be from a class derived from
+ * {@link RectangularShape}, but can also be from the class
+ * {@link Line2D}. <strong>Any other class risks firing a
+ * {@link ClassCastException} at execution</strong>.
+ *
+ * The default implementation always returns a
+ * {@link java.awt.geom.Rectangle2D} object.
+ *
+ * @param  event Logical coordinates of the mouse at the moment the button
+ *         is pressed. This information can be used by derived classes
+ *         that wish to consider the mouse position before choosing a
+ *         geometric shape.
+ * @return Shape from the class {link RectangularShape} or {link Line2D},
+ *         or <code>null</code> to indicate that we do not want to select
+ *         with the mouse.
+ */
+protected Shape getMouseSelectionShape(final Point2D point) {
+    return new Rectangle2D.Float();
}
+
+/**
+ * Indique si la loupe est visible. Par défaut, la loupe n'est pas visible.
+ * Appelez {@link #setMagnifierVisible(boolean)} pour la faire apparaitre.
+ */
+public boolean isMagnifierVisible() {
+    return magnifier!=null;
}
+
+/**
+ * Indicates whether or not the magnifying glass is visible.  By default,
+ * it is not visible. Call {@link #setMagnifierVisible(boolean)} to make it
+ * appear.
+ */
+public boolean isMagnifierVisible() {
+    return magnifier!=null;
}
+
+/**
+ * Fait apparaître ou disparaître la loupe. Si la loupe n'était pas visible
+ * et que cette méthode est appelée avec l'argument <code>true</code>, alors
+ * la loupe apparaîtra au centre de la fenêtre.
+ */
+public void setMagnifierVisible(final boolean visible) {
+    setMagnifierVisible(visible, null);
}
+
+/**
+ * Displays or hides the magnifying glass. If the magnifying glass is not
+ * visible and this method is called with the argument <code>true</code>,
+ * the magnifying glass will appear at the centre of the window.
+ */
+public void setMagnifierVisible(final boolean visible) {
+    setMagnifierVisible(visible, null);
}
+
+/**
+ * Indique si l'affichage de la loupe est autorisée sur
+ * cette composante. Par défaut, elle est autorisée.
+ */
+public boolean isMagnifierEnabled() {
+    return magnifierEnabled;
}
+
+/**
+ * Indicates whether or not the magnifying glass is allowed to be
+ * displayed on this component.  By default, it is allowed.
+ */
+public boolean isMagnifierEnabled() {
+    return magnifierEnabled;
}
+
+/**
+ * Spécifie si l'affichage de la loupe est autorisée sur cette composante.
+ * L'appel de cette méthode avec la valeur <code>false</code> fera
+ * disparaître la loupe, supprimera le choix "Afficher la loupe" du menu
+ * contextuel et fera ignorer tous les appels à
+ * <code>{@link #setMagnifierVisible setMagnifierVisible}(true)</code>.
+ */
+public void setMagnifierEnabled(final boolean enabled) {
+    magnifierEnabled=enabled;
}
+
+/**
+ * Specifies whether or not the magnifying glass is allowed to be displayed
+ * on this component. Calling this method with the value <code>false</code>
+ * will hide the magnifying glass, delete the choice "Display magnifying
+ * glass" from the contextual menu and lead to all calls to
+ * <code>{@link #setMagnifierVisible setMagnifierVisible}(true)</code>
+ * being ignored.
+ */
+public void setMagnifierEnabled(final boolean enabled) {
+    magnifierEnabled=enabled;
}
+
+/**
+ * Corrige les coordonnées d'un pixel pour tenir compte de la présence de la
+ * loupe. La point <code>point</code> doit contenir les coordonnées d'un
+ * pixel à l'écran. Si la loupe est visible et que <code>point</code> se
+ * trouve sur cette loupe, alors ses coordonnées seront corrigées pour faire
+ * comme si elle pointait sur le même pixel, mais en l'absence de la loupe.
+ * En effet, la présence de la loupe peut déplacer la position apparante des
+ * pixels.
+ */
+public final void correctPointForMagnifier(final Point2D point) {
+    if (magnifier!=null && magnifier.contains(point)) {
}
+
+/**
+ * Corrects a pixel's coordinates to take into account the presence of the
+ * magnifying glass. The point <code>point</code> must contain the
+ * coordinates of a pixel on the screen. If the magnifying glass is visible
+ * and <code>point</code> falls within it, point's coordinates will be
+ * corrected to make as if it pointed at the pixel itself, but in the
+ * absence of the magnifying glass. In effect, the presence of the
+ * magnifying glass can move the apparent position of the pixels.
+ */
+public final void correctPointForMagnifier(final Point2D point) {
+    if (magnifier!=null && magnifier.contains(point)) {
final double centerX = magnifier.getCenterX();
+final double centerY = magnifier.getCenterY();
+/*
+ * Le code suivant est équivalent au transformations ci-dessous.
+ * Ces transformations doivent être identiques à celles qui sont
+ * appliquées dans {@link #paintMagnifier}.
+ *
+ *         translate(+centerX, +centerY);
+ *         scale    (magnifierPower, magnifierPower);
final double centerX = magnifier.getCenterX();
+final double centerY = magnifier.getCenterY();
+/*
+ * The following code is equivalent to the following
+ * transformations.
+ * These transformations must be identical to those which
+ * are applied in {@link #paintMagnifier}.
+ *
+ *         translate(+centerX, +centerY);
+ *         scale    (magnifierPower, magnifierPower);
}
+
+/**
+ * Fait apparaître ou disparaître la loupe. Si la loupe n'était pas visible
+ * et que cette méthode est appelée avec l'argument <code>true</code>, alors
+ * la loupe apparaîtra centré sur la coordonnées spécifiée.
+ *
+ * @param visible <code>true</code> pour faire apparaître la loupe,
+ *                ou <code>false</code> pour la faire disparaître.
+ * @param center  Coordonnée centrale à laquelle faire apparaître la loupe.
+ *                Si la loupe était initialement invisible, elle apparaîtra
+ *                centrée à cette coordonnée (ou au centre de l'écran si
+ *                <code>center</code> est nul). Si la loupe était déjà
+ *                visible et que <code>center</code> est non-nul, alors elle
+ *                sera déplacée pour la centrer à la coordonnées spécifiée.
+ */
+private void setMagnifierVisible(final boolean visible, final Point center) {
+    if (visible && magnifierEnabled) {
+        if (magnifier==null) {
+            Rectangle bounds=getZoomableBounds(); // Do not modifiy the Rectangle!
+            if (bounds.isEmpty()) bounds=new Rectangle(0,0,DEFAULT_SIZE,DEFAULT_SIZE);
+            final int size=Math.min(Math.min(bounds.width, bounds.height), DEFAULT_MAGNIFIER_SIZE);
+            final int centerX, centerY;
}
+
+/**
+ * Displays or hides the magnifying glass. If the magnifying glass isn't
+ * visible and this method is called with the argument <code>true</code>,
+ * the magnifying glass will be displayed centred on the specified
+ * coordinate.
+ *
+ * @param visible <code>true</code> to display the magnifying glass or
+ *                <code>false</code> to hide it.
+ * @param center  Central coordinate on which to display the magnifying
+ *                glass.  If the magnifying glass was initially invisible,
+ *                it will appear centred on this coordinate (or in the
+ *                centre of the screen if <code>center</code> is null). If
+ *                the magnifying glass was already visible and
+ *                <code>center</code> is not null, it will be moved to
+ *                centre it on the specified coordinate.
+ */
+private void setMagnifierVisible(final boolean visible, final Point center) {
+    if (visible && magnifierEnabled) {
+        if (magnifier==null) {
+            Rectangle bounds = getZoomableBounds(); // Do not modify the Rectangle!
+            if (bounds.isEmpty()) bounds=new Rectangle(0,0,DEFAULT_SIZE,DEFAULT_SIZE);
+            final int size=Math.min(Math.min(bounds.width, bounds.height), DEFAULT_MAGNIFIER_SIZE);
+            final int centerX, centerY;
}
+
+/**
+ * Ajoute au menu spécifié des options de navigations. Des menus
+ * tels que "Zoom avant" et "Zoom arrière" seront automatiquement
+ * ajoutés au menu avec les raccourcis-clavier appropriés.
+ */
+public void buildNavigationMenu(final JMenu menu) {
+    buildNavigationMenu(menu, null);
}
+
+/**
+ * Adds navigation options to the specified menu. Menus such as
+ * "Zoom in" and "Zoom out" will be automatically added to the menu
+ * together with the appropriate short-cut keys.
+ */
+public void buildNavigationMenu(final JMenu menu) {
+    buildNavigationMenu(menu, null);
}
+
+/**
+ * Ajoute au menu spécifié des options de navigations. Des menus
+ * tels que "Zoom avant" et "Zoom arrière" seront automatiquement
+ * ajoutés au menu avec les raccourcis-clavier appropriés.
+ */
+private void buildNavigationMenu(final JMenu menu, final JPopupMenu popup) {
+    int groupIndex=0;
}
+
+/**
+ * Adds navigation options to the specified menu. Menus such as
+ * "Zoom in" and "Zoom out" will be automatically added to the menu
+ * together with the appropriate short-cut keys.
+ */
+private void buildNavigationMenu(final JMenu menu, final JPopupMenu popup) {
+    int groupIndex=0;
final Action action=actionMap.get(ACTION_ID[i]);
+if (action!=null && action.getValue(Action.NAME)!=null) {
+    /*
+     * Vérifie si le prochain item fait parti d'un nouveau groupe.
+     * Si c'est la cas, il faudra ajouter un séparateur avant le
+     * prochain menu.
+     */
+    final int lastGroupIndex=groupIndex;
+    while ((ACTION_TYPE[i] & GROUP[groupIndex]) == 0) {
+        groupIndex = (groupIndex+1) % GROUP.length;
+        if (groupIndex==lastGroupIndex) break;
+    }
+    /*
+     * Ajoute un item au menu.
+     */
+    if (menu!=null) {
+        if (groupIndex!=lastGroupIndex) menu.addSeparator();
final Action action=actionMap.get(ACTION_ID[i]);
+if (action!=null && action.getValue(Action.NAME)!=null) {
+    /*
+     * Checks whether the next item belongs to a new group.
+     * If this is the case, it will be necessary to add a separator
+     * before the next menu.
+     */
+    final int lastGroupIndex=groupIndex;
+    while ((ACTION_TYPE[i] & GROUP[groupIndex]) == 0) {
+        groupIndex = (groupIndex+1) % GROUP.length;
+        if (groupIndex == lastGroupIndex) {
+            break;
+    }
+    }
+    /*
+     * Adds an item to the menu.
+     */
+    if (menu!=null) {
+        if (groupIndex!=lastGroupIndex) menu.addSeparator();
}
+
+/**
+ * Menu avec une position. Cette classe retient les coordonnées
+ * exacte de l'endroit où a cliqué l'utilisateur lorsqu'il a
+ * invoké ce menu.
+ *
+ * @author Martin Desruisseaux
+ * @version 1.0
}
+
+/**
+ * Menu with a position.  This class retains the exact coordinates of the
+ * place the user clicked when this menu was invoked.
+ *
+ * @author Martin Desruisseaux
+ * @version 1.0
 */
+private static final class PointPopupMenu extends JPopupMenu {
+    /**
+     * Coordonnées de l'endroit où
+     * avait cliqué l'utilisateur.
+     */
+    public final Point point;
+
+    /**
+     * Construit un menu en retenant
+     * la coordonnée spécifiée.
+     */
+    public PointPopupMenu(final Point point) {
+        this.point=point;
 */
+private static final class PointPopupMenu extends JPopupMenu {
+    /**
+     * Coordinates of the point the user clicked on.
+     */
+    public final Point point;
+
+    /**
+     * Constructs a menu, retaining the specified coordinate.
+     */
+    public PointPopupMenu(final Point point) {
+        this.point=point;
}
+
+/**
+ * Méthode appelée automatiquement lorsque l'utilisateur a cliqué sur le
+ * bouton droit de la souris. L'implémentation par défaut fait apparaître
+ * un menu contextuel dans lequel figure des options de navigations.
+ *
+ * @param  event Evénement de la souris contenant entre autre les
+ *         coordonnées pointées.
+ * @return Le menu contextuel, ou <code>null</code> pour ne pas faire
+ *         apparaître de menu.
+ */
+protected JPopupMenu getPopupMenu(final MouseEvent event) {
+    if (getZoomableBounds().contains(event.getX(), event.getY())) {
}
+
+/**
+ * Method called automatically when the user clicks on the right mouse
+ * button.  The default implementation displays a contextual menu
+ * containing navigation options.
+ *
+ * @param  event Mouse event containing amongst others, the
+ *         coordinates pointées???????????.
+ * @return The contextual menu, or <code>null</code> to avoid displaying
+ *         the menu.
+ */
+protected JPopupMenu getPopupMenu(final MouseEvent event) {
+    if (getZoomableBounds().contains(event.getX(), event.getY())) {
}
+
+/**
+ * Méthode appelée automatiquement lorsque l'utilisateur a cliqué sur le
+ * bouton droit de la souris à l'intérieur de la loupe. L'implémentation
+ * par défaut fait apparaître un menu contextuel dans lequel figure des
+ * options relatives à la loupe.
+ *
+ * @param  event Evénement de la souris contenant entre autre les
+ *         oordonnées pointées.
+ * @return Le menu contextuel, ou <code>null</code> pour ne pas faire
+ *         apparaître de menu.
+ */
+protected JPopupMenu getMagnifierMenu(final MouseEvent event) {
+    final Resources resources = Resources.getResources(getLocale());
}
+
+/**
+ * Method called automatically when the user clicks on the right mouse
+ * button inside the magnifying glass. The default implementation displays
+ * a contextual menu which contains magnifying glass options.
+ *
+ * @param  event Mouse event containing amongst others, the
+ *         coordinates ???? pointées.
+ * @return The contextual menu, or <code>null</code> to avoid displaying
+ *         the menu.
+ */
+protected JPopupMenu getMagnifierMenu(final MouseEvent event) {
+    final Resources resources = Resources.getResources(getLocale());
}
+
+/**
+ * Fait apparaître le menu contextuel de navigation, à la
+ * condition que l'évènement de la souris soit bien celui
+ * qui fait normalement apparaître ce menu.
+ */
+private void mayShowPopupMenu(final MouseEvent event) {
+    if ( event.getID()       == MouseEvent.MOUSE_PRESSED &&
}
+
+/**
+ * Displays the navigation contextual menu, provided the mouse event is
+ * in fact the one which normally displays this menu.
+ */
+private void mayShowPopupMenu(final MouseEvent event) {
+    if ( event.getID()       == MouseEvent.MOUSE_PRESSED &&
    SwingUtilities.convertPointToScreen(point, source);
+    screen.width  -= (size.width  + insets.right);
+    screen.height -= (size.height + insets.bottom);
+    if (point.x > screen.width)  point.x = screen.width;
+    if (point.y > screen.height) point.y = screen.height;
+    if (point.x < insets.left)   point.x = insets.left;
+    if (point.y < insets.top)    point.y = insets.top;
+    SwingUtilities.convertPointFromScreen(point, source);
+    popup.show(source, point.x, point.y);
+}
    SwingUtilities.convertPointToScreen(point, source);
+    screen.width  -= (size.width  + insets.right);
+    screen.height -= (size.height + insets.bottom);
+    if (point.x > screen.width) {
+        point.x = screen.width;
+    }
+    if (point.y > screen.height) {
+        point.y = screen.height;
+    }
+    if (point.x < insets.left) {
+        point.x = insets.left;
+    }
+    if (point.y < insets.top) {
+        point.y = insets.top;
+    }
+    SwingUtilities.convertPointFromScreen(point, source);
+    popup.show(source, point.x, point.y);
+}
}
+
+/**
+ * Méthode appelée automatiquement lorsque l'utilisateur a fait
+ * tourné la roulette de la souris. Cette méthode effectue un
+ * zoom centré sur la position de la souris.
+ */
+private final void mouseWheelMoved(final MouseWheelEvent event)
+{
}
+
+/**
+ * Method called automatically when user moves the mouse wheel. This method
+ * performs a zoom centred on the mouse position.
+ */
+private final void mouseWheelMoved(final MouseWheelEvent event)
+{
}
+
+/**
+ * Méthode appelée chaque fois que la dimension
+ * ou la position de la composante a changée.
+ */
+private final void processSizeEvent(final ComponentEvent event)
+{
}
+
+/**
+ * Method called each time the size or the position of the component
+ * changes.
+ */
+private final void processSizeEvent(final ComponentEvent event)
+{
    magnifier.setClip(getZoomableBounds());
+}
+/*
+ * On n'appelle par {@link #repaint} parce qu'il y a déjà une commande
+ * {@link #repaint} dans la queue.  Ainsi, le retraçage sera deux fois
+ * plus rapide sous le JDK 1.3. On n'appele pas {@link #transform} non
+ * plus car le zoom n'a pas vraiment changé;  on a seulement découvert
+ * une partie de la fenêtre qui était cachée. Mais il faut tout de même
+ * ajuster les barres de défilements.
+ */
+final Object[] listeners=listenerList.getListenerList();
+for (int i=listeners.length; (i-=2)>=0;) {
    magnifier.setClip(getZoomableBounds());
+}
+/*
+ * {@link #repaint} isn't called because there is already a
+ * {@link #repaint} command in the queue.  Therefore, the redraw will
+ * be twice as quick under JDK 1.3. {@link #transform} isn't called
+ * either because the zoom hasn't really changed; we have simply
+ * discovered a part of the window which was hidden before. However,
+ * we still need to adjust the scrollbars.
+ */
+final Object[] listeners=listenerList.getListenerList();
+for (int i=listeners.length; (i-=2)>=0;) {
}
+
+/**
+ * Retourne un objet qui affiche ce <code>ZoomPane</code>
+ * avec des barres de défilements.
+ */
+public JComponent createScrollPane() {
+    return new ScrollPane();
}
+
+/**
+ * Returns an object which displays this <code>ZoomPane</code>
+ * with the scrollbars.
+ */
+public JComponent createScrollPane() {
+    return new ScrollPane();
/**
+ * The scroll panel for {@link ZoomPane}. The standard {@link JScrollPane}
+ * class is not used because it is difficult to get {@link JViewport} to
+ * cooperate with transformation already handled by {@link ZoomPane#zoom}.
+ *
+ * @version 1.0
+ * @author Martin Desruisseaux
/**
+ * The scroll panel for {@link ZoomPane}. The standard {@link JScrollPane}
+ * class is not used because it is difficult to get {@link JViewport} to
+ * cooperate with transformations already handled by {@link ZoomPane#zoom}.
+ *
+ * @version 1.0
+ * @author Martin Desruisseaux
 */
+private final class ScrollPane extends JComponent implements PropertyChangeListener {
+    /**
+     * The horizontal scrolling bar, or <code>null</code> if none.
+     */
+    private final JScrollBar scrollbarX;
+
+    /**
+     * The vertical scrolling bar, or <code>null</code> if none.
+     */
+    private final JScrollBar scrollbarY;
+
+    /**
+     * Construct a scroll pane for the enclosing {@link ZoomPane}.
+     */
+    public ScrollPane() {
+        setOpaque(false);
+        setLayout(new GridBagLayout());
+        /*
+         * Setup the scroll bars.
+         */
+        if ((type & TRANSLATE_X)!=0) {
+            scrollbarX=new JScrollBar(JScrollBar.HORIZONTAL);
 */
+private final class ScrollPane extends JComponent implements PropertyChangeListener {
+    /**
+     * The horizontal scrollbar, or <code>null</code> if none.
+     */
+    private final JScrollBar scrollbarX;
+
+    /**
+     * The vertical scrollbar, or <code>null</code> if none.
+     */
+    private final JScrollBar scrollbarY;
+
+    /**
+     * Constructs a scroll pane for the enclosing {@link ZoomPane}.
+     */
+    public ScrollPane() {
+        setOpaque(false);
+        setLayout(new GridBagLayout());
+        /*
+         * Sets up the scrollbars.
+         */
+        if ((type & TRANSLATE_X)!=0) {
+            scrollbarX=new JScrollBar(JScrollBar.HORIZONTAL);
    scrollbarY  = null;
+}
+/*
+ * Add the scroll bars in the scroll pane.
+ */
+final Rectangle bounds = getZoomableBounds(); // Cached Rectangle: do not modify.
+final GridBagConstraints c = new GridBagConstraints();
    scrollbarY  = null;
+}
+/*
+ * Adds the scrollbars in the scroll pane.
+ */
+final Rectangle bounds = getZoomableBounds(); // Cached Rectangle: do not modify.
+final GridBagConstraints c = new GridBagConstraints();
}
+
+/**
+ * Convenience method fetching a scroll bar model.
+ * Should be a static method, but compiler doesn't
+ * allow this.
+ */
}
+
+/**
+ * Convenience method which fetches a scrollbar model.
+ * Should be a static method, but compiler doesn't
+ * allow this.
+ */
/**
+ * Invoked when this <code>ScrollPane</code>  is added in
+ * a {@link Container}. This method register all required
+ * listeners.
+ */
+public void addNotify() {
/**
+ * Invoked when this <code>ScrollPane</code>  is added in
+ * a {@link Container}. This method registers all required
+ * listeners.
+ */
+public void addNotify() {
/**
+ * Invoked when this <code>ScrollPane</code> is removed from
+ * a {@link Container}. This method unregister all listeners.
+ */
+public void removeNotify() {
+    ZoomPane.this.removePropertyChangeListener("zoom.insets", this);
/**
+ * Invoked when this <code>ScrollPane</code> is removed from
+ * a {@link Container}. This method unregisters all listeners.
+ */
+public void removeNotify() {
+    ZoomPane.this.removePropertyChangeListener("zoom.insets", this);
}
+
+/**
+ * Invoked when the zoomable area changed. This method will adjust
+ * scroll bar's insets in order to keep scroll bars aligned in front
+ * of the zoomable area.
+ *
+ * Note: in current version, this is an undocumented capability.
+ *       Class {@link RangeBar} use it, but it is experimental.
+ *       It may change in a future version.
+ */
+public void propertyChange(final PropertyChangeEvent event) {
}
+
+/**
+ * Invoked when the zoomable area changes. This method will adjust
+ * scroll bar's insets in order to keep scroll bars aligned in front
+ * of the zoomable area.
+ *
+ * Note: in the current version, this is an undocumented capability.
+ *       Class {@link RangeBar} uses it, but it is experimental.
+ *       It may change in a future version.
+ */
+public void propertyChange(final PropertyChangeEvent event) {
}
+
+/**
+ * Synchronise la position et l'étendu des models <var>x</var> et
+ * <var>y</var> avec la position du zoom. Les models <var>x</var>
+ * et <var>y</var> sont généralement associés à des barres de defilements
+ * horizontale et verticale. Lorsque la position d'une barre de défilement
+ * est ajustée, le zomm sera ajusté en conséquence. Inversement, lorsque le
+ * zoom est modifié, les positions et étendus des barres de défilements sont
+ * ajustées en conséquence.
+ *
+ * @param x Modèle de la barre de défilement horizontale,
+ *          ou <code>null</code> s'il n'y en a pas.
+ * @param y Modèle de la barre de défilement verticale,
+ *          ou <code>null</code> s'il n'y en a pas.
+ */
+public void tieModels(final BoundedRangeModel x, final BoundedRangeModel y) {
+    if (x!=null || y!=null) {
}
+
+/**
+ * Synchronises the position and the range of the models <var>x</var> and
+ * <var>y</var> with the position of the zoom.  The models <var>x</var>
+ * and <var>y</var> are generally associated with horizontal and vertical
+ * scrollbars.  When the position of a scrollbar is adjusted, the zoom is
+ * consequently adjusted. Inversely, when the zoom is modified, the
+ * positions and ranges of the scrollbars are consequently adjusted.
+ *
+ * @param x Model of the horizontal scrollbar or <code>null</code> if there
+ *          isn't one.
+ * @param y Model of the vertical scrollbar or <code>null</code> if there
+ *          isn't one.
+ */
+public void tieModels(final BoundedRangeModel x, final BoundedRangeModel y) {
+    if (x!=null || y!=null) {
}
+
+/**
+ * Annule la synchronisation entre les models <var>x</var> et <var>y</var>
+ * spécifiés et le zoom de cet objet <code>ZoomPane</code>. Les objets
+ * {@link ChangeListener} et {@link ZoomChangeListener} qui avait été créés
+ * seront supprimés.
+ *
+ * @param x Modèle de la barre de défilement horizontale,
+ *          ou <code>null</code> s'il n'y en a pas.
+ * @param y Modèle de la barre de défilement verticale,
+ *          ou <code>null</code> s'il n'y en a pas.
+ */
+public void untieModels(final BoundedRangeModel x, final BoundedRangeModel y) {
+    final EventListener[] listeners=getListeners(ZoomChangeListener.class);
}
+
+/**
+ * Cancels the synchronisation between the specified <var>x</var> and
+ * <var>y</var> models and the zoom of this <code>ZoomPane</code> object.
+ * The {@link ChangeListener} and {@link ZoomChangeListener} objects that
+ * were created are deleted.
+ *
+ * @param x Model of the horizontal scrollbar or <code>null</code> if there
+ *          isn't one.
+ * @param y Model of the vertical scrollbar or <code>null</code> if there
+ *          isn't one.
+ */
+public void untieModels(final BoundedRangeModel x, final BoundedRangeModel y) {
+    final EventListener[] listeners=getListeners(ZoomChangeListener.class);
}
+
+/**
+ * Objet ayant la charge de synchronizer un objet {@link JScrollPane}
+ * avec des barres de défilements. Bien que ce ne soit généralement pas
+ * utile, il serait possible de synchroniser plusieurs paires d'objets
+ * {@link BoundedRangeModel} sur un  même objet <code>ZoomPane</code>.
+ *
+ * @author Martin Desruisseaux
+ * @version 1.0
}
+
+/**
+ * Object responsible for synchronizing a {@link JScrollPane} object with
+ * scrollbars.  Whilst not generally useful, it would be possible to
+ * synchronize several pairs of {@link BoundedRangeModel} objects on one
+ * <code>ZoomPane</code> object.
+ *
+ * @author Martin Desruisseaux
+ * @version 1.0
 */
+private final class Synchronizer implements ChangeListener, ZoomChangeListener {
+    /**
+     * Modèle à synchroniser avec {@link ZoomPane}.
+     */
+    public final BoundedRangeModel xm,ym;
+
+    /**
+     * Indique si les barres de défilements sont en train
+     * d'être ajustées en réponse à {@link #zoomChanged}.
+     * Si c'est la cas, {@link #stateChanged} ne doit pas
+     * faire d'autres ajustements.
+     */
+    private transient boolean isAdjusting;
+
+    /**
+     * Cached <code>ZoomPane</code> bounds. Used in order
+     * to avoid two many object allocation on the heap.
+     */
+    private transient Rectangle bounds;
+
+    /**
+     * Construit un objet qui synchronisera une paire de
+     * {@link BoundedRangeModel} avec {@link ZoomPane}.
+     */
+    public Synchronizer(final BoundedRangeModel xm, final BoundedRangeModel ym) {
+        this.xm = xm;
 */
+private final class Synchronizer implements ChangeListener, ZoomChangeListener {
+    /**
+     * Model to synchronize with {@link ZoomPane}.
+     */
+    public final BoundedRangeModel xm,ym;
+
+    /**
+     * Indicates whether the scrollbars are being adjusted in
+     * response to {@link #zoomChanged}.
+     * If this is the case, {@link #stateChanged} mustn't make any
+     * other adjustments.
+     */
+    private transient boolean isAdjusting;
+
+    /**
+     * Cached <code>ZoomPane</code> bounds. Used in order
+     * to avoid too many object allocations on the heap.
+     */
+    private transient Rectangle bounds;
+
+    /**
+     * Constructs an object which synchronises a pair of
+     * {@link BoundedRangeModel} with {@link ZoomPane}.
+     */
+    public Synchronizer(final BoundedRangeModel xm, final BoundedRangeModel ym) {
+        this.xm = xm;
}
+
+/**
+ * Méthode appelée automatiquement chaque fois que la
+ * position d'une des barres de défilement a changée.
+ */
+public void stateChanged(final ChangeEvent event) {
+    if (!isAdjusting) {
}
+
+/**
+ * Method called automatically each time the position of one of the
+ * scrollbars changes.
+ */
+public void stateChanged(final ChangeEvent event) {
+    if (!isAdjusting) {
final boolean valueIsAdjusting=((BoundedRangeModel) event.getSource()).getValueIsAdjusting();
+if (paintingWhileAdjusting || !valueIsAdjusting) {
+    /*
+     * Scroll view coordinates are computed with the following
+     * steps:
+     *
+     *   1) Get the logical coordinates for the whole area.
+     *   2) Transform to pixel space using current zoom.
+     *   3) Clip to the scroll bars position (in pixels).
+     *   4) Transform back to the logical space.
+     *   5) Set the visible area to the resulting rectangle.
+     */
final boolean valueIsAdjusting=((BoundedRangeModel) event.getSource()).getValueIsAdjusting();
+if (paintingWhileAdjusting || !valueIsAdjusting) {
+    /*
+     * Scroll view coordinates are computed using the
+     * following steps:
+     *
+     *   1) Get the logical coordinates for the whole area.
+     *   2) Transform to pixel space using current zoom.
+     *   3) Clip to the scrollbar's position (in pixels).
+     *   4) Transform back to the logical space.
+     *   5) Set the visible area to the resulting rectangle.
+     */
}
+
+/**
+ * Méthode appelée chaque fois que le zoom a changé.
+ *
+ * @param change Ignoré. Peut être nul, et sera
+ *               effectivement parfois nul.
+ */
+public void zoomChanged(final ZoomChangeEvent change) {
+    if (!isAdjusting) {
}
+
+/**
+ * Method called each time the zoom changes.
+ *
+ * @param change Ignored. Can be null and will effectively sometimes
+ *               be null.
+ */
+public void zoomChanged(final ZoomChangeEvent change) {
+    if (!isAdjusting) {
}
+
+/**
+ * Procède à l'ajustement des valeurs d'un model. Les minimums et maximums
+ * seront ajustés au besoin afin d'inclure la valeur et son étendu. Cet
+ * ajustement est nécessaire pour éviter un comportement chaotique lorsque
+ * l'utilisateur fait glisser l'ascensceur pendant qu'une partie du
+ * graphique est en dehors de la zone qui était initialement prévue par
+ * {@link #getArea}.
+ */
+private static void setRangeProperties(final BoundedRangeModel model,
+                                       final double value, final int extent,
}
+
+/**
+ * Adjusts the values of a model.  The minimums and maximums are adjusted
+ * as needed in order to include the value and its range. This adjustment
+ * is necessary in order to avoid chaotic behaviour when the user
+ * drags the slider whilst a part of the graphic is outside the zone
+ * initially planned for {@link #getArea}.
+ */
+private static void setRangeProperties(final BoundedRangeModel model,
+                                       final double value, final int extent,
}
+
+/**
+ * Modifie la position en pixels de la partie visible de
+ * <code>ZoomPanel</code>. Soit <code>viewSize</code> les dimensions en
+ * pixels qu'aurait <code>ZoomPane</code> si sa surface visible couvrait
+ * la totalité de la région {@link #getArea} avec le zoom courant (Note:
+ * cette dimension <code>viewSize</code> peut être obtenues par {@link
+ * #getPreferredSize} si {@link #setPreferredSize} n'a pas été appelée avec
+ * une valeur non-nulle). Alors par définition la région {@link #getArea}
+ * convertit dans l'espace des pixels donnerait le rectangle
+ *
+ * <code>bounds=Rectangle(0,&nbsp;0,&nbsp;,viewSize.width,&nbsp;,viewSize.height)</code>.
+ *
+ * Cette méthode <code>scrollRectToVisible</code> permet de définir la
+ * sous-région de <code>bounds</code> qui doit apparaître dans la fenêtre
+ * <code>ZoomPane</code>.
+ */
+public void scrollRectToVisible(final Rectangle rect) {
+    Rectangle2D area=getArea();
+    if (isValid(area)) {
+        area=XAffineTransform.transform(zoom, area, null);
+        area.setRect(area.getX()+rect.getX(), area.getY()+rect.getY(), rect.getWidth(), rect.getHeight());
+        try {
+            setVisibleArea(XAffineTransform.inverseTransform(zoom, area, area));
+        } catch (NoninvertibleTransformException exception) {
}
+
+/**
+ * Modifies the position in pixels of the visible part of
+ * <code>ZoomPane</code>. <code>viewSize</code> is the size
+ * <code>ZoomPane</code> would be (in pixels) if its visible surface
+ * covered the whole of the {@link #getArea} region with the current
+ * zoom (Note: <code>viewSize</code> can be obtained by {@link
+ * #getPreferredSize} if {@link #setPreferredSize} hasn't been called
+ * with a non-null value). Therefore, by definition, the region
+ * {@link #getArea} converted into pixel space would give the rectangle
+ * <code>bounds=Rectangle(0,&nbsp;0,&nbsp;,viewSize.width,&nbsp;,viewSize.height)</code>.
+ *
+ * This <code>scrollRectToVisible</code> method allows us to define the
+ * sub-region of <code>bounds</code> which must appear in the
+ * <code>ZoomPane</code> window.
+ */
+public void scrollRectToVisible(final Rectangle rect) {
+    Rectangle2D area=getArea();
+    if (isValid(area)) {
+        area=XAffineTransform.transform(zoom, area, null);
+        area.setRect(area.getX() + rect.getX(), area.getY() + rect.getY(),
+                     rect.getWidth(), rect.getHeight());
+        try {
+            setVisibleArea(XAffineTransform.inverseTransform(zoom, area, area));
+        } catch (NoninvertibleTransformException exception) {
}
+
+/**
+ * Indique si cet objet <code>ZoomPane</code> doit être redessiné pendant
+ * que l'utilisateur déplace le glissoir des barres de défilements. Les
+ * barres de défilements (ou autres models) concernées sont celles qui ont
+ * été synchronisées avec cet objet <code>ZoomPane</code> à l'aide de la
+ * méthode {@link #tieModels}. La valeur par défaut est <code>false</code>,
+ * ce qui signifie que <code>ZoomPane</code> attendra que l'utilisateur ait
+ * relaché le glissoir avant de se redessiner.
+ */
+public boolean isPaintingWhileAdjusting() {
+    return paintingWhileAdjusting;
}
+
+/**
+ * Indicates whether or not this <code>ZoomPane</code> object should be
+ * repainted when the user moves the scrollbar slider. The scrollbars (or
+ * other models) involved are those which have been synchronised with
+ * this <code>ZoomPane</code> object through the {@link #tieModels} method.
+ * The default value is <code>false</code>, which means that
+ * <code>ZoomPane</code> will wait until the user releases the slider
+ * before repainting.
+ */
+public boolean isPaintingWhileAdjusting() {
+    return paintingWhileAdjusting;
}
+
+/**
+ * Définit si cet objet <code>ZoomPane</code> devra redessiner la carte
+ * pendant que l'utilisateur déplace le glissoir des barres de défilements.
+ * Il vaut mieux avoir un ordinateur assez rapide pour donner la valeur
+ * <code>true</code> à ce drapeau.
+ */
+public void setPaintingWhileAdjusting(final boolean flag) {
+    paintingWhileAdjusting = flag;
}
+
+/**
+ * Defines whether or not this <code>ZoomPane</code> object should repaint
+ * the map when the user moves the scrollbar slider.
+ * A fast computer is recommended if this flag is to be set to
+ * <code>true</code>.
+ */
+public void setPaintingWhileAdjusting(final boolean flag) {
+    paintingWhileAdjusting = flag;
}
+
+/**
+ * Déclare qu'une partie de ce paneau a besoin d'être redéssinée. Cette
+ * méthode ne fait que redéfinir la méthode de la classe parente pour tenir
+ * compte du cas où la loupe serait affichée.
+ */
+public void repaint(final long tm, final int x, final int y,
+                    final int width, final int height) {
+    super.repaint(tm, x, y, width, height);
+    if (magnifier!=null && magnifier.intersects(x,y,width,height)) {
+        // Si la partie à dessiner est à l'intérieur de la loupe,
+        // le fait que la loupe fasse un agrandissement nous oblige
+        // à redessiner un peu plus que ce qui avait été demandé.
+        repaintMagnifier();
+    }
+}
+
+/**
+ * Déclare que la loupe a besoin d'être redéssinée. Une commande
+ * {@link #repaint()} sera envoyée avec comme coordonnées les limites
+ * de la loupe (en tenant compte de sa bordure).
+ */
+private void repaintMagnifier() {
+    final Rectangle bounds=magnifier.getBounds();
}
+
+/**
+ * Declares that a part of this pane needs to be repainted. This method
+ * simply redefines the method of the parent class in order to take into
+ * account a case where the magnifying glass is displayed.
+ */
+public void repaint(final long tm, final int x, final int y,
+                    final int width, final int height) {
+    super.repaint(tm, x, y, width, height);
+    if (magnifier!=null && magnifier.intersects(x,y,width,height)) {
+        // If the part to paint is inside the magnifying glass,
+        // the fact that the magnifying glass is zooming in means
+        // we have to repaint a little more than that which was requested.
+        repaintMagnifier();
+    }
+}
+
+/**
+ * Declares that the magnifying glass needs to be repainted. A
+ * {@link #repaint()} command is sent with the bounds of the
+ * magnifying glass as coordinates (taking into account its
+ * outline).
+ */
+private void repaintMagnifier() {
+    final Rectangle bounds=magnifier.getBounds();
}
+
+/**
+ * Paints the magnifier. This method is invoked after
+ * {@link #paintComponent(Graphics2D)} if a magnifier
+ * is visible.
+ */
+protected void paintMagnifier(final Graphics2D graphics) {
}
+
+/**
+ * Paints the magnifying glass. This method is invoked after
+ * {@link #paintComponent(Graphics2D)} if a magnifying glass
+ * is visible.
+ */
+protected void paintMagnifier(final Graphics2D graphics) {
graphics.setColor (magnifierBorder);
+graphics.draw     (magnifier);
+graphics.setStroke(stroke);
+graphics.clip     (magnifier); // Coordonnées en pixels!
+graphics.setColor (magnifierColor);
+graphics.fill     (magnifier.getBounds2D());
+graphics.setPaint (paint);
graphics.setColor (magnifierBorder);
+graphics.draw     (magnifier);
+graphics.setStroke(stroke);
+graphics.clip     (magnifier); // Coordinates in pixels!
+graphics.setColor (magnifierColor);
+graphics.fill     (magnifier.getBounds2D());
+graphics.setPaint (paint);
    graphics.translate(+centerX, +centerY);
+    graphics.scale    (magnifierPower, magnifierPower);
+    graphics.translate(-centerX, -centerY);
+    // Note: les transformations effectuées ici doivent être identiques
+    //       à celles qui sont faites dans {@link #pixelToLogical}.
+    paintComponent    (graphics);
+}
+
+/**
+ * Paints this component. Subclass must override this method in order to
+ * drawn the <code>ZoomPane</code> content. For must implementations, the
+ * first line in this method will be
+ *
+ * <code>graphics.transform({@link #zoom})</code>.
    graphics.translate(+centerX, +centerY);
+    graphics.scale    (magnifierPower, magnifierPower);
+    graphics.translate(-centerX, -centerY);
+    // Note: the transformations performed here must be identical to those
+    //       performed in {@link #pixelToLogical}.
+    paintComponent    (graphics);
+}
+
+/**
+ * Paints this component. Subclass must override this method in order to
+ * draw the <code>ZoomPane</code> content. For most implementations, the
+ * first line in this method will be
+ *
+ * <code>graphics.transform({@link #zoom})</code>.
/**
+ * Paints this component. This method is declared <code>final</code>
+ * in order to avoir unintentional overriding. Override
+ * {@link #paintComponent(Graphics2D)} instead.
+ */
+protected final void paintComponent(final Graphics graphics) {
/**
+ * Paints this component. This method is declared <code>final</code>
+ * in order to avoid unintentional overriding. Override
+ * {@link #paintComponent(Graphics2D)} instead.
+ */
+protected final void paintComponent(final Graphics graphics) {
flag=IS_PAINTING;
+super.paintComponent(graphics);
+/*
+ * La méthode <code>JComponent.paintComponent(...)</code> crée un objet <code>Graphics2D</code>
+ * temporaire, puis appelle <code>ComponentUI.update(...)</code> avec en paramètre ce graphique.
+ * Cette méthode efface le fond de l'écran, puis appelle <code>ComponentUI.paint(...)</code>.
+ * Or, cette dernière a été redéfinie plus haut (notre objet {@link #UI}) de sorte qu'elle
+ * appelle elle-même {@link #paintComponent(Graphics2D)}. Un chemin compliqué, mais on a pas
+ * tellement le choix et c'est somme toute assez efficace.
+ */
+if (magnifier!=null) {
+    flag=IS_PAINTING_MAGNIFIER;
flag=IS_PAINTING;
+super.paintComponent(graphics);
+/*
+ * The <code>JComponent.paintComponent(...)</code> method creates a
+ * temporary <code>Graphics2D</code> object, then calls
+ * <code>ComponentUI.update(...)</code> with this graphic as a
+ * parameter.  This method clears the screen background then calls
+ * <code>ComponentUI.paint(...)</code>.
+ * This last method has been redefined further up (our {@link #UI})
+ * object in such a way that it calls itself
+ * {@link #paintComponent(Graphics2D)}. A complicated path, but we
+ * don't have much choice and it is, after all, quite efficient.
+ */
+if (magnifier!=null) {
+    flag=IS_PAINTING_MAGNIFIER;
/**
+ * Prints this component. This method is declared <code>final</code>
+ * in order to avoir unintentional overriding. Override
+ * {@link #printComponent(Graphics2D)} instead.
+ */
+protected final void printComponent(final Graphics graphics) {
/**
+ * Prints this component. This method is declared <code>final</code>
+ * in order to avoid unintentional overriding. Override
+ * {@link #printComponent(Graphics2D)} instead.
+ */
+protected final void printComponent(final Graphics graphics) {
}
+
+/**
+ * Retourne la dimension (en pixels) qu'aurait <code>ZoomPane</code> s'il
+ * affichait la totalité de la région {@link #getArea} avec le zoom courant
+ * ({@link #zoom}). Cette méthode est pratique pour déterminer les valeurs
+ * maximales à affecter aux barres de défilement. Par exemple la barre
+ * horizontale pourrait couvrir la plage <code>[0..viewSize.width]</code>
+ * tandis que la barre verticale pourrait couvrir la plage
+ * <code>[0..viewSize.height]</code>.
+ */
+private final Dimension getViewSize() {
}
+
+/**
+ * Returns the size (in pixels) that <code>ZoomPane</code> would have if
+ * it displayed the whole of the {@link #getArea} region with the current
+ * zoom ({@link #zoom}). This method is practical for determining the
+ * maximum values to assign to the scrollbars. For example, the horizontal
+ * bar could cover the range <code>[0..viewSize.width]</code>
+ * whilst the vertical bar could cover the range
+ * <code>[0..viewSize.height]</code>.
+ */
+private final Dimension getViewSize() {
}
+
+/**
+ * Retourne les marges de cette composante. Cette méthode fonctionne comme

[... 130 lines stripped ...]