/* * Constellation - An open source and standard compliant SDI * http://www.constellation-sdi.org * * Copyright 2014 Geomatys. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.constellation.gui; // User interface import org.constellation.resources.i18n.ResourceKeys; import org.constellation.resources.i18n.Resources; import org.geotoolkit.display.axis.AbstractGraduation; import org.geotoolkit.display.axis.Axis2D; import org.geotoolkit.display.axis.DateGraduation; import org.geotoolkit.display.axis.Graduation; import org.geotoolkit.display.axis.NumberGraduation; import org.geotoolkit.gui.swing.ExceptionMonitor; import org.geotoolkit.gui.swing.ZoomPane; import org.geotoolkit.util.collection.RangeSet; import org.geotoolkit.util.converter.ConverterRegistry; import javax.media.jai.util.Range; import javax.swing.*; import javax.swing.border.Border; import javax.units.Unit; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.text.Format; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.TimeZone; // Graphics // Geometry // Collections // Miscellaneous // Geotools dependencies // Axis and graduation //import org.geotools.units.Unit; // Resources /** * Paneau représentant les plages des données disponibles. Ces plages sont * représentées par des barres verticales. L'axe des <var>x</var> représente * les valeurs, et sur l'axe des <var>y</var> on place les différents types * de données, un peu comme le ferait un histogramme. * * <p> </p> * <p align="center"><img src="doc-files/RangeBars.png"></p> * <p> </p> * * @version $Id$ * @author Martin Desruisseaux */ @SuppressWarnings("serial") public class RangeBars extends ZoomPane { /** * Small value for floating point comparaison. */ private static final double EPS = 1E-12; /** * Constant for the horizontal orientation. * Labels and bars are both horizontal. */ public static final int HORIZONTAL = SwingConstants.HORIZONTAL; /** * Constant for the vertical orientation. * Labels and bars are both vertical. */ public static final int VERTICAL = SwingConstants.VERTICAL; /** * Constant for the vertical orientation. * Bars are vertical, but labels still horizontal. */ public static final int VERTICAL_EXCEPT_LABELS = 2; /** * Margin (in pixels) to left in the window after a call to {@link #reset}. * Set to 0 in order to use fully all the available area. */ private static final int RESET_MARGIN = 12; /** * An affine transform for applying a 90° rotation on text labels. */ private static final AffineTransform ROTATE_90 = new AffineTransform(0, 1, -1, 0, 0, 0); /** * Données des barres. Chaque entré est constitué d'une paire * (<em>étiquette</em>, <em>tableau de données</em>). Le tableau de * donnée sera généralement (mais pas obligatoirement) un tableau de * type {@code long[]}. Les données de ces tableaux seront organisées * par paires de valeurs, de la forme (<i>début</i>,<i>fin</i>). */ private final Map<String,RangeSet> ranges = new LinkedHashMap<String,RangeSet>(); /** * Axe des <var>x</var> servant à écrire les valeurs des plages. Les * méthodes de {@link Axis2D} peuvent être appellées pour modifier le format * des nombres, une étiquette, des unités ou pour spécifier "à la main" les * minimums et maximums. */ private final Axis2D axis; /** * Valeur minimale à avoir été spécifiée avec {@link #addRange}. * Cette valeur n'est pas valide si <code>(minimum<maximum)</code> * est {@code false}. Cette valeur peut etre calculée par un * appel à {@link #ensureValidGlobalRange}. */ private transient double minimum; /** * Valeur maximale à avoir été spécifiée avec {@link #addRange}. * Cette valeur n'est pas valide si <code>(minimum<maximum)</code> * est {@code false}. Cette valeur peut etre calculée par un * appel à {@link #ensureValidGlobalRange}. */ private transient double maximum; /** * Zoomable bounds in pixel coordinates. This boundind box is computed from the widget * bounds minus the {@linkplain #labelBounds label bounding box}. The computation is * performed by: * * <blockquote><pre> * {@code zoomableBounds = getZoomableBounds(zoomableBounds);} * </pre></blockquote> */ private transient Rectangle zoomableBounds; /** * Coordonnées (en pixels) de la région dans laquelle seront dessinées * les étiquettes. Ce champ est nul si ces coordonnées ne sont pas encore * connues. Ces coordonnées sont calculées par * * {@link #paintComponent(Graphics2D)} * * (notez que cette dernière accepte un argument {@link Graphics2D} nul). */ private transient Rectangle labelBounds; /** * Coordonnées (en pixels) de la région dans laquelle sera dessinée l'axe. * Ce champ est nul si ces coordonnées ne sont pas encore connues. Ces * coordonnées sont calculées par {@link #paintComponent(Graphics2D)} * (notez que cette dernière accepte un argument {@link Graphics2D} nul). */ private transient Rectangle axisBounds; /** * Indique si cette composante sera orientée horizontalement ou * verticalement. */ private final boolean horizontal; /** * {@code true} if labels should be vertical as well when the * {@code RangeBars} component is vertical. */ private final boolean verticalLabels; /** * Indique si la méthode {@link #reset} a été appelée * sur cet objet avec une dimension valide de la fenêtre. */ private boolean valid; /** * Espaces (en pixels) à laisser de chaque côtés * du graphique. Ces dimensions seront retournées * par {@link #getInsets}. */ private short top=12, left=12, bottom=6, right=15; /** * Hauteur (en pixels) des barres des histogrammes. */ private final short barThickness=12; /** * Espace (en pixels) entre les étiquettes et leurs barres. */ private final short barOffset=6; /** * Espace (en pixels) à ajouter entre deux lignes. */ private final short lineSpacing=6; /** * Empirical value (in pixels) to add to {@code labelBounds.width} * after painting vertical bars. I cant' understand why it is needed! */ private static final int XOFFSET_FOR_VERTICAL_BARS = 4; /** * The background color for the zoomable area (default to white). * This is different from the widget's background color (default * to gray), which is specified with {@link #setBackground(Color)}. */ private final Color backgbColor = Color.white; /** * The bars color (default to orange). */ private final Color barColor = new Color(255, 153, 51); /** * The slider color. Default to a transparent purple. */ private final Color selColor = new Color(128, 64, 92, 96); /* * There is no field for label color. Label color can * be specified with {@link #setForeground(Color)}. */ /** * The border to paint around the zoomable area. */ private final Border border = BorderFactory.createEtchedBorder(); /** * Plage de valeurs présentement sélectionnée par l'utilisateur. Cette * plage apparaîtra comme un rectangle transparent (une <em>visière</em>) * par dessus les barres. Ce champ est initialement nul. Une visière ne * sera créée que lorsqu'elle sera nécessaire. */ private transient MouseReshapeTracker slider; /** * Modèle permettant de décrire la position de la visière par un entier. * Ce model est fournit pour faciliter les interactions avec <i>Swing</i>. * Ce champ peut être nul si aucun model n'a encore été demandé. */ private transient SwingModel swingModel; /** * Point utilisé temporairement lors * des transformations affine. */ private transient Point2D.Double point; /** * Objet {@link #insets} à réutiliser autant que possible. */ private transient Insets insets; /** * Construit un paneau initialement vide qui représentera des * nombres sans unités. Des données pourront être ajoutées avec * la méthode {@link #addRange} pour faire apparaître des barres. */ public RangeBars() { this((Unit)null, HORIZONTAL); } /** * Construit un paneau initialement vide qui représentera des * nombres selon les unités spécifiées. Des données pourront * être ajoutées avec la méthode {@link #addRange} pour faire * apparaître des barres. * * @param unit Unit of measure, or {@code null}. * @param orientation Either {@link #HORIZONTAL}, {@link #VERTICAL} * or {@link #VERTICAL_EXCEPT_LABELS}. */ public RangeBars(final Unit unit, final int orientation) { this(new NumberGraduation(null/*unit*/), // TODO isHorizontal (orientation), isVerticalLabels(orientation)); } /** * Construit un paneau initialement vide qui représentera des * dates dans le fuseau horaire spécifié. Des données pourront * être ajoutées avec la méthode {@link #addRange} pour faire * apparaître des barres. * * @param timezone The timezone. * @param orientation Either {@link #HORIZONTAL}, {@link #VERTICAL} * or {@link #VERTICAL_EXCEPT_LABELS}. */ public RangeBars(final TimeZone timezone, final int orientation) { this(new DateGraduation(timezone), isHorizontal (orientation), isVerticalLabels(orientation)); } /** * Construit un paneau initialement vide. Des données pourront * être ajoutées avec la méthode {@link #addRange} pour faire * apparaître des barres. */ private RangeBars(final AbstractGraduation graduation, final boolean horizontal, final boolean verticalLabels) { super(horizontal ? (TRANSLATE_X|SCALE_X|RESET) : (TRANSLATE_Y|SCALE_Y|RESET)); this.horizontal = horizontal; this.verticalLabels = verticalLabels; axis = new Axis2D(graduation); axis.setLabelClockwise(horizontal); axis.setRenderingHint(Graduation.AXIS_TITLE_FONT, new Font("SansSerif", Font.BOLD, 11)); axis.setRenderingHint(Graduation.TICK_LABEL_FONT, new Font("SansSerif", Font.PLAIN, 10)); LookAndFeel.installColors(this, "Label.background", "Label.foreground"); setMagnifierEnabled(false); /* * Resizing vertical bars is trickier than resizing horizontal bars, * because vertical bars are aligned on maximal X (right aligned) while * horizontal bars are aligned on minimal Y (top aligned). It is easier * to simply clear the cache on component resize. */ if (!horizontal) { addComponentListener(new ComponentAdapter() { public void componentResized(final ComponentEvent event) { clearCache(); } }); } setPaintingWhileAdjusting(true); } /** * Check the orientation. * * @param orientation Either {@link #HORIZONTAL}, {@link #VERTICAL} * or {@link #VERTICAL_EXCEPT_LABELS}. */ private static boolean isHorizontal(final int orientation) { switch (orientation) { case HORIZONTAL: return true; case VERTICAL: return false; case VERTICAL_EXCEPT_LABELS: return false; default: throw new IllegalArgumentException(); } } /** * Check the labels orientation. * * @param orientation Either {@link #HORIZONTAL}, {@link #VERTICAL} * or {@link #VERTICAL_EXCEPT_LABELS}. */ private static boolean isVerticalLabels(final int orientation) { switch (orientation) { case HORIZONTAL: return false; case VERTICAL: return true ; case VERTICAL_EXCEPT_LABELS: return false; default: throw new IllegalArgumentException(); } } /** * Set the timezone for graduation label. This affect only the way * labels are displayed. This method can be invoked only if this * {@code RangeBars} has been constructed with the * {@code RangeBars(TimeZone)} constructor. * * @param timezone The new time zone. * @throws IllegalStateException if this {@code RangeBars} has has * not been constructed with the {@code RangeBars(TimeZone)} * constructor. */ public void setTimeZone(final TimeZone timezone) { final Graduation graduation = axis.getGraduation(); if (graduation instanceof DateGraduation) { final DateGraduation dateGrad = (DateGraduation) graduation; final TimeZone oldTimezone = dateGrad.getTimeZone(); dateGrad.setTimeZone(timezone); clearCache(); repaint(); firePropertyChange("timezone", oldTimezone, timezone); } else { throw new IllegalStateException(); } } /** * Efface toutes les barres qui étaient tracées. */ public synchronized void clear() { ranges.clear(); clearCache(); repaint(); } /** * Efface les barres correspondant à l'étiquette spécifiée. */ public synchronized void remove(final String label) { ranges.remove(label); clearCache(); repaint(); } /** * Ajoute une plage de valeurs. Chaque plage de valeurs est associée à une * étiquette. Il est possible de spécifier (dans n'importe quel ordre) * plusieurs plages à une même étiquette. Si deux plages se chevauchent * pour une étiquette donnée, elles seront fusionnées ensemble. * * @param label Etiquette désignant la barre pour laquelle on veut ajouter * une plage. Si cette étiquette avait déjà été utilisée * précédemment, les données seront ajoutées à la barre déjà * existante. Sinon, une nouvelle barre sera créée. Les * différences entres majuscules et minuscules sont prises * en compte. La valeur {@code null} est autorisée. * @param first Début de la plage. * @param last Fin de la plage. * * @throws NullPointerException Si {@code first} ou {@code last} est nul. * @throws IllegalArgumentException Si {@code first} et {@code last} * ne sont pas de la même classe, ou s'ils ne sont pas de la classe * des éléments précédemment mémorisés sous l'étiquette {@code label}. */ public synchronized void addRange(final String label, final Comparable first, final Comparable last) { RangeSet rangeSet = ranges.get(label); if (rangeSet == null) { rangeSet = new RangeSet(first.getClass()); ranges.put(label, rangeSet); } rangeSet.add(first, last); clearCache(); repaint(); } /** * Définit les plages de valeurs pour l'étiquette spécifiée. * Les anciennes plages de valeurs pour cette étiquette seront * oubliées. * * @param label Etiquette pour laquelle définir une plage de valeur. * @param newRanges Nouvelle plage de valeurs. */ public synchronized void setRanges(final String label, final RangeSet newRanges) { if (newRanges != null) { ranges.put(label, newRanges); clearCache(); repaint(); } else { remove(label); } } /** * Update {@link #minimum} and {@link #maximum} value if it was not already * done. If minimum and maximum was already up to date, then nothing will * be done. This update is performed using all intervals specified to this * {@code RangeBars}. * * @return {@code true} if {@link #minimum} and {@link #maximum} are * valid after this call, or {@code false} if an update was * necessary but failed for whatever reasons (for example because * there is no intervals in this {@code RangeBars}). */ private boolean ensureValidGlobalRange() { if (minimum < maximum) { return true; } double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY; for (final RangeSet rangeSet : ranges.values()) { final int size = rangeSet.size(); if (size != 0) { double tmp; if ((tmp=rangeSet.getMinValueAsDouble(0 )) < min) min=tmp; if ((tmp=rangeSet.getMaxValueAsDouble(size-1)) > max) max=tmp; } } if (min < max) { this.minimum = min; this.maximum = max; return true; } return false; } /** * Déclare qu'un changement a été fait et que ce changement * peut nécessiter le recalcul d'informations conservées * dans une cache interne. */ private void clearCache() { minimum = Double.NaN; maximum = Double.NaN; labelBounds = null; axisBounds = null; if (swingModel != null) { swingModel.revalidate(); } } /** * Spécifie la légende de l'axe. La valeur {@code null} * signifie qu'il ne faut pas afficher de légende. */ public void setLegend(final String label) {// No 'synchronized' needed here ((AbstractGraduation) axis.getGraduation()).setTitle(label); } /** * Retourne la légende de l'axe. */ public String getLegend() { // No 'synchronized' needed here return axis.getGraduation().getTitle(false); } /** * Retourne la liste des étiquettes en mémoire, dans l'ordre dans lequel * elles seront écrites. Le tableau retourné est une copie des tableaux * internes. En conséquence, les changements faits sur ce tableau n'auront * pas de répercussions sur {@code this}. */ public synchronized String[] getLabels() { return ranges.keySet().toArray(new String[ranges.size()]); } /** * Retourne la valeur minimale mémorisée. Si plusieurs étiquettes ont été * spécifiées, elles seront tous prises en compte. Si aucune valeur n'a été * mémorisée dans cet objet, alors cette méthode retourne {@code null}. */ public synchronized Comparable getMinimum() { return getMinimum(getLabels()); } /** * Retourne la valeur maximale mémorisée. Si plusieurs étiquettes ont été * spécifiées, elles seront tous prises en compte. Si aucune valeur n'a été * mémorisée dans cet objet, alors cette méthode retourne {@code null}. */ public synchronized Comparable getMaximum() { return getMaximum(getLabels()); } /** * Retourne la valeur minimale mémorisée sous l'étiquette spécifiée. Si aucune * donnée n'a été mémorisée sous cette étiquette, retourne {@code null}. */ public Comparable getMinimum(final String label) { return getMinimum(new String[] {label}); } /** * Retourne la valeur maximale mémorisée sous l'étiquette spécifiée. Si aucune * donnée n'a été mémorisée sous cette étiquette, retourne {@code null}. */ public Comparable getMaximum(final String label) { return getMaximum(new String[] {label}); } /** * Retourne la valeur minimale mémorisée sous les étiquettes spécifiées. * Si aucune donnée n'a été mémorisée sous ces étiquettes, retourne * {@code null}. */ public synchronized Comparable getMinimum(final String[] labels) { Comparable min = null; for (int i=0; i<labels.length; i++) { final RangeSet rangeSet = ranges.get(labels[i]); if (!rangeSet.isEmpty()) { final Comparable tmp = ((Range)rangeSet.first()).getMinValue(); if (min==null || min.compareTo(tmp)>0) { min = tmp; } } } return min; } /** * Retourne la valeur maximale mémorisée sous les étiquettes spécifiées. * Si aucune donnée n'a été mémorisée sous ces étiquettes, retourne * {@code null}. */ public synchronized Comparable getMaximum(final String[] labels) { Comparable max = null; for (int i=0; i<labels.length; i++) { final RangeSet rangeSet = ranges.get(labels[i]); if (!rangeSet.isEmpty()) { final Comparable tmp = ((Range)rangeSet.last()).getMaxValue(); if (max==null || max.compareTo(tmp)<0) { max = tmp; } } } return max; } /** * Déclare qu'on aura besoin d'une visière. Cette méthode Vérifie que * {@code slider} est non-nul. S'il était nul, une nouvelle visière * sera créée et positionnée. Si on n'avait pas assez d'informations pour * positionner la visière, sa création sera annulée. */ private void ensureSliderCreated() { if (slider != null) { return; } slider = new MouseReshapeTracker() { /** Invoked after the position and size of the visor has changed. */ protected void stateChanged(final boolean isAdjusting) { if (swingModel != null) { swingModel.fireStateChanged(isAdjusting); } } /** Invoked when a change in the clip is required (e.g. user edited a field). */ protected void clipChangeRequested(double xmin, double xmax, double ymin, double ymax) { setVisibleRange(xmin, xmax, ymin, ymax); } }; addMouseListener(slider); addMouseMotionListener(slider); /* * Si un modèle existait, on l'utilisera pour * définir la position initiale de la visière. * Sinon, on construira un nouveau modèle. */ if (swingModel == null) { if (ensureValidGlobalRange()) { final double min = this.minimum; final double max = this.maximum; if (horizontal) { slider.setX(min, min+0.25*(max-min)); } else { slider.setY(min, min+0.25*(max-min)); } } } else { swingModel.synchronize(); } } /** * Retourne la valeur au centre de la * plage sélectionnée par l'utilisateur. */ public double getSelectedValue() { if (slider == null) { return Double.NaN; } return horizontal ? slider.getCenterX() : slider.getCenterY(); } /** * Retourne la valeur au début de la * plage sélectionnée par l'utilisateur. */ public double getMinSelectedValue() { if (slider == null) { return Double.NaN; } return horizontal ? slider.getMinX() : slider.getMinY(); } /** * Retourne la valeur à la fin de la * plage sélectionnée par l'utilisateur. */ public double getMaxSelectedValue() { if (slider == null) { return Double.NaN; } return horizontal ? slider.getMaxX() : slider.getMaxY(); } /** * Spécifie la plage de valeurs à sélectionner. * Cette plage de valeurs apparaîtra comme un * rectangle transparent superposé aux barres. */ public void setSelectedRange(final double min, final double max) { ensureSliderCreated(); repaint(slider.getBounds()); if (horizontal) { slider.setX(min, max); } else { slider.setY(min, max); } /* * Déclare que la position de la visière à changée. * Les barres seront redessinées et le model sera * prévenu du changement */ repaint(slider.getBounds()); if (swingModel != null) { swingModel.fireStateChanged(false); } } /** * Modifie le zoom du graphique de façon à faire apparaître la * plage de valeurs spécifiée. Si l'intervale spécifié n'est pas * entièrement compris dans la plage des valeurs en mémoire, cette * méthode décalera et/ou zoomera l'intervale spécifié de façon à * l'inclure dans la plage des valeurs en mémoire. * * @param min Valeur minimale. * @param max Valeur maximale. */ public void setVisibleRange(final double min, final double max) { if (horizontal) { setVisibleRange(min, max, Double.NaN, Double.NaN); } else { setVisibleRange(Double.NaN, Double.NaN, min, max); } } /** * Modifie le zoom du graphique de façon à faire apparaître la * plage de valeurs spécifiée. Si l'intervale spécifié n'est pas * entièrement compris dans la plage des valeurs en mémoire, cette * méthode décalera et/ou zoomera l'intervale spécifié de façon à * l'inclure dans la plage des valeurs en mémoire. */ private void setVisibleRange(double xmin, double xmax, double ymin, double ymax) { if (ensureValidGlobalRange()) { final double minimim = this.minimum; final double maximum = this.maximum; final Insets insets = this.insets = getInsets(this.insets); final int top = insets.top; final int left = insets.left; final int bottom = insets.bottom; final int right = insets.right; if (horizontal) { /* * Note: "xmax -= (xmin-minimum)" is an abreviation of * "xmax = (xmax-xmin) + minimum". Setting new * values for "xmin" and "xmax" is an intentional * side effect of "if" clause, to be run only if * the first "if" term is true. */ if (xmin<minimum && maximum<(xmax -= xmin-(xmin=minimum))) xmax=maximum; if (xmax>maximum && minimum>(xmin -= xmax-(xmax=maximum))) xmin=minimum; if (xmin < xmax) { setVisibleArea(new Rectangle2D.Double(xmin, top, xmax-xmin, Math.max(bottom-top, barThickness))); if (slider != null) { final int height = Math.max(barThickness, getZoomableHeight()); slider.setClipMinMax(xmin, xmax, top, top+height); } } } else { if (ymin<minimum && maximum<(ymax -= ymin-(ymin=minimum))) ymax=maximum; if (ymax>maximum && minimum>(ymin -= ymax-(ymax=maximum))) ymin=minimum; if (ymin < ymax) { final int rightAlign = Math.max(getWidth()-right, left); final int width = Math.max(barThickness, getZoomableWidth()); setVisibleArea(new Rectangle2D.Double(rightAlign-width, ymin, Math.max(rightAlign, barThickness), ymax-ymin)); if (slider != null) { slider.setClipMinMax(rightAlign-width, rightAlign, ymin, ymax); } } } } } /** * Returns {@code true} if user is allowed to edit * or drag the slider's dimension. If {@code false}, * then the user can change the slider's location but not * its dimension. */ public boolean isRangeAdjustable() { if (slider == null) { return false; } if (horizontal) { return slider.isAdjustable(SwingConstants.EAST) || slider.isAdjustable(SwingConstants.WEST); } else { return slider.isAdjustable(SwingConstants.NORTH) || slider.isAdjustable(SwingConstants.SOUTH); } } /** * Specify if the user is allowed to edit or drag the slider's dimension. * If {@code true}, then the user is allowed to change both slider's * dimension and location. If {@code false}, then the user is allowed * to change slider's location only. */ public void setRangeAdjustable(final boolean b) { ensureSliderCreated(); if (horizontal) { slider.setAdjustable(SwingConstants.EAST, b); slider.setAdjustable(SwingConstants.WEST, b); } else { slider.setAdjustable(SwingConstants.NORTH, b); slider.setAdjustable(SwingConstants.SOUTH, b); } } /** * Set the font for labels and graduations. This font is applied "as is" * to labels. However, graduations will use a slightly smaller and plain * font, even if the specified font was in bold or italic. */ public void setFont(final Font font) { super.setFont(font); axis.setRenderingHint(Graduation.AXIS_TITLE_FONT, font); final int size = font.getSize(); axis.setRenderingHint(Graduation.TICK_LABEL_FONT, font.deriveFont(Font.PLAIN, size-(size>=14 ? 2 : 1))); clearCache(); } /** * Retourne le nombre de pixels à laisser entre la région dans laquelle les * barres sont dessinées et les bords de cette composante. <strong>Notez que * les marges retournées par {@code getInsets(Insets)} peuvent etre plus * grandes que celles qui ont été spécifiées à {@link #setInsets}.</strong> * Un espace suplémentaire peut avoir ajouté pour tenir compte d'une * éventuelle bordure qui aurait été ajoutée à la composante. * * @param insets Objet à réutiliser si possible, ou {@code null}. * @return Les marges à laisser de chaque côté de la zone de traçage. */ public Insets getInsets(Insets insets) { insets = super.getInsets(insets); insets.top += top; insets.left += left; insets.bottom += bottom; insets.right += right; return insets; } /** * Défini le nombre de pixels à laisser entre la région dans laquelle les * barres sont dessinées et les bords de cette composante. Ce nombre de * pixels doit être suffisament grand pour laisser de la place pour les * étiquettes de l'axe. Notez que {@link #getInsets} ne va pas * obligatoirement retourner exactement ces marges. */ public void setInsets(final Insets insets) { top = (short) insets.top; left = (short) insets.left; bottom = (short) insets.bottom; right = (short) insets.right; repaint(); } /** * Returns the bounding box (in pixel coordinates) of the zoomable area. * This implementation returns bounding box covering only a sub-area of * this widget area, because space is needed for axis and labels. An extra * margin of {@link #getInsets} is also reserved. * * @param bounds An optional pre-allocated rectangle, or {@code null} * to create a new one. This argument is useful if the caller * wants to avoid allocating a new object on the heap. * @return The bounding box of the zoomable area, in pixel coordinates * relative to this {@code RangeBars} widget. */ protected Rectangle getZoomableBounds(Rectangle bounds) { bounds = super.getZoomableBounds(bounds); /* * 'labelBounds' is the rectangle (in pixels) where legends are going * to be displayed. If this rectangle has not been computed yet, it * can be computed now with 'paintComponent(null)'. */ if (labelBounds == null) { if (!valid) { reset(bounds); } paintComponent(null, bounds.width + (left+right), bounds.height + (top+bottom)); if (labelBounds == null) { return bounds; } } if (horizontal) { bounds.x += labelBounds.width; bounds.width -= labelBounds.width; bounds.height = labelBounds.height; // No changes to bounds.y: align on top. } else { final int width = getZoomableWidth(); bounds.y += labelBounds.height; bounds.height -= labelBounds.height; bounds.x += bounds.width - width; // Align right. bounds.width = width; } return bounds; } /** * Returns the width of the zoomable area. This method do not trigger * zoomable bounds computation if bounds was not readily available. */ private int getZoomableWidth() { if (horizontal || verticalLabels) { return (labelBounds!=null) ? labelBounds.width : getWidth(); } return (barThickness+lineSpacing)*ranges.size() + XOFFSET_FOR_VERTICAL_BARS; } /** * Returns the height of the zoomable area. This method do not trigger * zoomable bounds computation if bounds was not readily available. */ private int getZoomableHeight() { return (labelBounds!=null) ? labelBounds.height : getHeight(); } /** * Returns the default size for this component. This is the size * returned by {@link #getPreferredSize} if no preferred size has * been explicitly set with {@link #setPreferredSize}. * * @return The default size for this component. */ protected Dimension getDefaultSize() { final Insets insets = this.insets = getInsets(this.insets); final int top = insets.top; final int left = insets.left; final int bottom = insets.bottom; final int right = insets.right; final Dimension size=super.getDefaultSize(); if (labelBounds==null || axisBounds==null) { if (!valid) { /* * Force immediate computation of an approximative affine * transform (for the zoom). A more precise affine transform * may be computed later. */ reset(new Rectangle(left, top, size.width - (left+right), size.height - (top+bottom))); } paintComponent(null, size.width, size.height); if (labelBounds==null || axisBounds==null) { size.width = 280; size.height = 60; return size; } } if (horizontal) { // height = [bottom of axis] - [top of labels] + [margin]. size.height = (axisBounds.y + axisBounds.height) - labelBounds.y + (bottom + top); } else { // width = [right of labels] - [left of axis] + [margin]. size.width = (labelBounds.x + labelBounds.width) - axisBounds.x + (right + left); } return size; } /** * Invoked when this component must be drawn but no data are available * yet. Default implementation paint the text "No data" in the middle * of the component. * * @param graphics The paint context to draw to. */ protected void paintNodata(final Graphics2D graphics) { graphics.setColor(getForeground()); final Resources resources = Resources.getResources(getLocale()); final String message = resources.getString(ResourceKeys.NO_DATA_TO_DISPLAY); final FontRenderContext fc = graphics.getFontRenderContext(); final GlyphVector glyphs = getFont().createGlyphVector(fc, message); final Rectangle2D bounds = glyphs.getVisualBounds(); graphics.drawGlyphVector(glyphs, (float) (0.5*(getWidth()-bounds.getWidth())), (float) (0.5*(getHeight()+bounds.getHeight()))); } /** * Draw the bars, labels and their graduation. Bars and labels are drawn in * the same order as they were specified to {@link #addRange}. * * @param graphics The paint context to draw to. */ protected void paintComponent(final Graphics2D graphics) { paintComponent(graphics, getWidth(), getHeight()); } /** * Implementation of {@link #paintComponent(Graphics2D)}. * This special implementation is invoked by {@link #getZoomableBounds}) * and {@link #getDefaultSize}. It is not too much a problem if this method * is not in synchronization with {@link #paintComponent(Graphics2D)} (for * example because the user overrided it). The user can fix the problem by * overriding {@link #getZoomableBounds}) and {@link #getDefaultSize} too. * * @param graphics The paint context to draw to. * @param componentWidth Width of this component. This information is usually * given by {@link #getWidth}, except when this method is invoked from * a method computing this component's dimension! * @param componentHeight Height of this component. This information is usually * given by {@link #getHeight}, except when this method is invoked from * a method computing this component's dimension! */ private void paintComponent(final Graphics2D graphics, final int componentWidth, final int componentHeight) { final int rangeCount = ranges.size(); if (rangeCount==0 || !ensureValidGlobalRange()) { if (graphics != null) { paintNodata(graphics); } return; } final Insets borderInsets = (border!=null) ? border.getBorderInsets(this) : null; final Insets insets = this.insets = getInsets(this.insets); final int top = insets.top; final int left = insets.left; final int bottom = insets.bottom; final int right = insets.right; final AbstractGraduation graduation = (AbstractGraduation) axis.getGraduation(); final GlyphVector[] glyphs = new GlyphVector[rangeCount]; final double[] labelAscent = new double [rangeCount]; final double[] labelWidth = (!horizontal && !verticalLabels) ? new double[rangeCount] : null; final Shape clip; final FontRenderContext fc; if (graphics == null) { clip = null; fc = new FontRenderContext(null, false, false); /* * Do not invoke reset() here because this block has probably * been executed in order to compute this component's size, * i.e. this method has probably been invoked by reset() itself! */ } else { if (!valid) { reset(); } clip = graphics.getClip(); fc = graphics.getFontRenderContext(); } /* * Setup an array of "glyph vectors" for labels. Gylph vectors will be * drawn later. Before drawing, we query all glyph vectors for their * size, then we compute a typical "slot" size that will be applied to * every label. */ double labelSlotWidth; double labelSlotHeight; if (clip==null || labelBounds==null || clip.intersects(labelBounds)) { Font font = getFont(); if (font == null) { font = UIManager.getFont("Panel.font"); if (font == null) { throw new IllegalStateException(); } } if (horizontal) { labelSlotWidth = 0; labelSlotHeight = barThickness; } else if (verticalLabels) { // Rotate font 90° font = font.deriveFont(ROTATE_90); labelSlotWidth = barThickness; labelSlotHeight = 0; } else { labelSlotWidth = 0; labelSlotHeight = 0; } final Iterator<String> it = ranges.keySet().iterator(); for (int i=0; i<rangeCount; i++) { final String label = it.next(); if (label != null) { glyphs[i] = font.createGlyphVector(fc, label); Rectangle2D rect = glyphs[i].getVisualBounds(); double height = rect.getHeight(); double width = rect.getWidth(); if (horizontal) { labelAscent[i] = height; } else if (verticalLabels) { labelAscent[i] = width; } else { labelWidth [i] = width; labelAscent[i] = height; width += i*(barThickness + lineSpacing); } if (width >labelSlotWidth ) labelSlotWidth =width; if (height>labelSlotHeight) labelSlotHeight=height; } } if (it.hasNext()) { // Should not happen throw new ConcurrentModificationException(); } if (labelBounds == null) { labelBounds = new Rectangle(); } if (horizontal) { labelSlotWidth += barOffset; labelSlotHeight += lineSpacing; labelBounds.setBounds(left, top, (int)Math.ceil(labelSlotWidth), (int)Math.ceil(labelSlotHeight*rangeCount)); } else if (verticalLabels) { labelSlotHeight += barOffset; labelSlotWidth += lineSpacing; labelBounds.setBounds(componentWidth-right, top, (int)Math.ceil(labelSlotWidth*rangeCount), (int)Math.ceil(labelSlotHeight)); labelBounds.width += XOFFSET_FOR_VERTICAL_BARS; // Empirical adjustement labelBounds.x -= labelBounds.width; } else { labelSlotHeight += lineSpacing/2; labelBounds.setBounds(componentWidth-right, top, (int)Math.ceil(labelSlotWidth), (int)Math.ceil(labelSlotHeight*rangeCount)); labelBounds.width += XOFFSET_FOR_VERTICAL_BARS; // Empirical adjustement labelBounds.x -= labelBounds.width; } } double barSlotSize; labelSlotWidth = labelBounds.getWidth(); labelSlotHeight = labelBounds.getHeight(); if (horizontal) { labelSlotHeight /= rangeCount; barSlotSize = labelSlotHeight; } else if (verticalLabels) { labelSlotWidth = (labelSlotWidth-XOFFSET_FOR_VERTICAL_BARS) / rangeCount; barSlotSize = labelSlotWidth; } else { labelSlotHeight /= rangeCount; barSlotSize = (barThickness + lineSpacing); } /* * Now, we know the space needed for all labels. It is time to compute * the axis position. This axis will be below horizontal bars or at the * right of vertical bars. We also calibrate the axis for its minimum * and maximum values, which are zoom dependent. */ try { Point2D.Double point = this.point; if (point == null) { this.point = point = new Point2D.Double(); } if (horizontal) { double y = labelBounds.getMaxY(); double x1 = labelBounds.getMaxX(); double x2 = componentWidth - right; /* * Compute the minimal logical value, * which is at the left of the axis. */ point.setLocation(x1, y); zoom.inverseTransform(point, point); graduation.setMinimum(point.x); if (point.x < minimum) { graduation.setMinimum(point.x=minimum); zoom.transform(point, point); x1 = point.x; } /* * Compute the maximal logical value, * which is at the right of the axis. */ point.setLocation(x2, y); zoom.inverseTransform(point, point); graduation.setMaximum(point.x); if (point.x > maximum) { graduation.setMaximum(point.x=maximum); zoom.transform(point, point); x2 = point.x; } axis.setLine(x1, y, x2, y); } else { double x = verticalLabels ? labelBounds.getMinX() : labelBounds.getMaxX() - getZoomableWidth(); double y1 = componentHeight - bottom; double y2 = labelBounds.getMaxY(); if (borderInsets != null) { x -= borderInsets.left; } /* * Compute the minimal logical value, * which is at the bottom of the axis. */ point.setLocation(x, y1); zoom.inverseTransform(point, point); graduation.setMinimum(point.y); if (point.y < minimum) { graduation.setMinimum(point.y=minimum); zoom.transform(point, point); y1 = point.y; } /* * Compute the maximal logical value, * which is at the top of the axis. */ point.setLocation(x, y2); zoom.inverseTransform(point, point); graduation.setMaximum(point.y); if (point.y > maximum) { graduation.setMaximum(point.y=maximum); zoom.transform(point, point); y2 = point.y; } axis.setLine(x, y1, x, y2); } } catch (NoninvertibleTransformException exception) { // Should not happen ExceptionMonitor.paintStackTrace(graphics, getBounds(), exception); return; } /* * Prepare the painting. Paint the border first, * then paint all labels. Paint bars next, and * paint axis last. */ if (graphics != null) { final Color foreground = getForeground(); final double clipMinimum = graduation.getMinimum(); final double clipMaximum = graduation.getMaximum(); zoomableBounds = getZoomableBounds(zoomableBounds); if (border != null) { border.paintBorder(this, graphics, zoomableBounds.x-borderInsets.left, zoomableBounds.y-borderInsets.top, zoomableBounds.width+(borderInsets.left+borderInsets.right), zoomableBounds.height+(borderInsets.top+borderInsets.bottom)); } graphics.setColor(foreground); for (int i=0; i<rangeCount; i++) { if (glyphs[i] != null) { float x,y; if (horizontal) { x = labelBounds.x; y = (float) (labelBounds.y + i*labelSlotHeight + 0.5*(labelSlotHeight+labelAscent[i])); } else if (verticalLabels) { y = labelBounds.y; x = (float) (labelBounds.x + i*labelSlotWidth + 0.5*labelAscent[i]); } else { x = (float) (labelBounds.x + labelBounds.width - (rangeCount-i)*barSlotSize); y = (float) (labelBounds.y + labelBounds.height - i*labelSlotHeight - 0.5*(labelSlotHeight+labelAscent[i])); final int ox = Math.round((float) (x + 0.5*barSlotSize)); final int oy = Math.round((float) (y - 0.5*labelAscent[i])); graphics.drawLine(Math.round(x)+3, oy, ox, oy); graphics.drawLine(ox, oy, ox, Math.round((float) (labelBounds.y + labelBounds.height))-3); x -= labelWidth[i]; } graphics.drawGlyphVector(glyphs[i], x, y); } } graphics.setColor(backgbColor); graphics.fill (zoomableBounds); graphics.clip (zoomableBounds); graphics.setColor(barColor); final Iterator<RangeSet> it=ranges.values().iterator(); final Rectangle2D.Double bar = new Rectangle2D.Double(); final double scale, translate; if (horizontal) { scale = zoom.getScaleX(); translate = zoom.getTranslateX(); bar.y = zoomableBounds.y + 0.5*(barSlotSize-barThickness); bar.height = barThickness; } else { scale = zoom.getScaleY(); translate = zoom.getTranslateY(); bar.x = zoomableBounds.x + 0.5*barThickness; bar.width = barThickness; } for (int i=0; i<rangeCount; i++) { final RangeSet rangeSet = it.next(); final int size = rangeSet.size(); for (int j=0; j<size; j++) { final double bar_min = rangeSet.getMinValueAsDouble(j); final double bar_max = rangeSet.getMaxValueAsDouble(j); if (bar_min > clipMaximum) break; // Slight optimization if (bar_max > clipMinimum) { if (horizontal) { bar.x = bar_min; bar.width = bar_max-bar_min; bar.width *= scale; bar.x *= scale; bar.x += translate; } else { bar.y = bar_max; bar.height = bar_min-bar_max; bar.height *= scale; bar.y *= scale; bar.y += translate; } graphics.fill(bar); } } if (horizontal) { bar.y += barSlotSize; } else { bar.x += barSlotSize; } } if (it.hasNext()) { // Should not happen throw new ConcurrentModificationException(); } graphics.setClip(clip); graphics.setColor(foreground); axis.paint(graphics); /* * The component is now fully painted. If a slider is visible, paint * the slider on top of everything else. The slider must always been * painted, no matter what 'MouseReshapeTracker.isEmpty()' said. */ if (slider != null) { if (swingModel != null) { swingModel.synchronize(); } if (horizontal) { final double ymin = zoomableBounds.getMinY(); final double ymax = zoomableBounds.getMaxY(); slider.setClipMinMax(clipMinimum, clipMaximum, ymin, ymax); slider.setY ( ymin, ymax); } else { final double xmin = zoomableBounds.getMinX(); final double xmax = zoomableBounds.getMaxX(); slider.setClipMinMax(xmin, xmax, clipMinimum, clipMaximum); slider.setX (xmin, xmax); } graphics.clip(zoomableBounds); graphics.transform(zoom); graphics.setColor(selColor); graphics.fill(slider); } } /* * Recompute axis bounds again. It has already been computed sooner, * but bounds may be more precise after painting. Next, we slightly * increase its size to avoid unpainted zones after {@link #repaint} * calls. */ axisBounds = axis.getBounds(); axisBounds.height++; } /** * Apply a transform on the {@linkplain #zoom zoom}. This method override * {@link ZoomPane#transform(AffineTransform)} in order to make sure that * the supplied transform will not get the bars out of the component. If * the transform would push all bars out, then it will not be applied. * * @param change The change to apply, in logical coordinates. * @throws UnsupportedOperationException if the transform {@code change} * contains an unsupported transformation, for example a vertical * translation while this component is drawing only horizontal bars. */ public void transform(final AffineTransform change) throws UnsupportedOperationException { /* * First, make sure that the transformation is a supported one. * Shear and rotation are not allowed. Scale is allowed only * along the main axis direction. */ if (!(Math.abs(change.getShearX() )<=EPS && Math.abs(change.getShearY() )<=EPS && horizontal ? (Math.abs(change.getScaleY()-1)<=EPS && Math.abs(change.getTranslateY())<=EPS) : (Math.abs(change.getScaleX()-1)<=EPS && Math.abs(change.getTranslateX())<=EPS))) { throw new UnsupportedOperationException("Unexpected transform"); } /* * Check if applying the transform would push all bars out * of the component. If so, then exit without applying the * transform. */ if (ensureValidGlobalRange() && (zoomableBounds=getZoomableBounds(zoomableBounds))!=null) { Point2D.Double point = this.point; if (point == null) { this.point = point = new Point2D.Double(); } if (horizontal) { final int xLeft = zoomableBounds.x; final int xRight = zoomableBounds.width + xLeft; final int margin = zoomableBounds.width / 4; final double x1, x2, y=zoomableBounds.getCenterY(); point.x=minimum; point.y=y; change.transform(point,point); zoom.transform(point,point); x1=point.x; point.x=maximum; point.y=y; change.transform(point,point); zoom.transform(point,point); x2=point.x; if (Math.min(x1,x2)>(xRight-margin) || Math.max(x1,x2)<(xLeft+margin) || Math.abs(x2-x1)<margin) { return; } } else { final int yTop = zoomableBounds.y; final int yBottom = zoomableBounds.height + yTop; final int margin = zoomableBounds.height / 4; final double y1, y2, x=zoomableBounds.getCenterX(); point.y=minimum; point.x=x; change.transform(point,point); zoom.transform(point,point); y1=point.y; point.y=maximum; point.x=x; change.transform(point,point); zoom.transform(point,point); y2=point.y; if (Math.min(y1,y2)>(yBottom-margin) || Math.max(y1,y2)<(yTop+margin) || Math.abs(y2-y1)<margin) { return; } } } /* * Applique la transformation, met à jour la transformation * de la visière et redessine l'axe en plus du graphique. */ super.transform(change); if (slider != null) { slider.setTransform(zoom); } if (axisBounds != null) { repaint(axisBounds); } } /** * Reset the zoom in such a way that every bars fit in the display area. */ public void reset() { reset(zoomableBounds=getZoomableBounds(zoomableBounds)); if (getWidth()>0 && getHeight()>0) { valid = true; } } /** * Reset the zoom in such a way that every bars fit in the specified display area. */ private void reset(Rectangle zoomableBounds) { if (RESET_MARGIN != 0) { zoomableBounds = (Rectangle) zoomableBounds.clone(); if (horizontal) { final int margin = Math.min(RESET_MARGIN, zoomableBounds.width/2); zoomableBounds.x += margin; zoomableBounds.width -= margin*2; } else { final int margin = Math.min(RESET_MARGIN, zoomableBounds.height/2); zoomableBounds.y += margin; zoomableBounds.height -= margin*2; } } reset(zoomableBounds, !horizontal); if (slider != null) { slider.setTransform(zoom); } if (axisBounds != null) { repaint(axisBounds); } } /** * Returns logical coordinates for the display area. */ public Rectangle2D getArea() { final Insets insets = this.insets = getInsets(this.insets); final int top = insets.top; final int left = insets.left; final int bottom = insets.bottom; final int right = insets.right; if (ensureValidGlobalRange()) { final double min = this.minimum; final double max = this.maximum; if (horizontal) { int height = getHeight(); if (height==0) { height = getMinimumSize().height; // Height doesn't need to be exact, // since it will be ignored anyway... } return new Rectangle2D.Double(min, top, max-min, Math.max(height-(top+bottom),16)); } else { int width = getWidth(); if (width==0) { width = getMinimumSize().width; // Width doesn't need to be exact, // since it will be ignored anyway... } return new Rectangle2D.Double(left, min, Math.max(width-(left+right),16), max-min); } } /* * This block will be run only if logical coordinate of display area * can't be computed, because of not having enough informations. It * make a simple guess, which is better than nothing. */ final Rectangle bounds = getBounds(); bounds.x = left; bounds.y = top; bounds.width -= (left+right); bounds.height -= (top+bottom); return bounds; } /** * Retourne un model pouvant décrire la position de la visière dans une * plage d'entiers. Ce model est fournit pour faciliter les interactions * avec <i>Swing</i>. Ses principales méthodes sont définies comme suit: * * <p>{@link BoundedRangeModel#getValue}<br> * Retourne la position du bord gauche de la visière, exprimée par * un entier compris entre le minimum et le maximum du model (0 et * 100 par défaut).</p> * * <p>{@link BoundedRangeModel#getExtent}<br> * Retourne la largeur de la visière, exprimée selon les mêmes unités * que {@code getValue()}.</p> * * <p>{@link BoundedRangeModel#setMinimum} / {@link BoundedRangeModel#setMaximum}<br> * Modifie les valeurs entière minimale ou maximale retournées par {@code getValue()}. * Cette modification n'affecte aucunement l'axe des barres affichées; elle * ne fait que modifier la façon dont la position de la visière est convertie * en valeur entière par {@code getValue()}.</p> * * <p>{@link BoundedRangeModel#setValue} / {@link BoundedRangeModel#setExtent}<br> * Modifie la position du bord gauche de la visière ou sa largeur.</p> */ public synchronized LogicalBoundedRangeModel getModel() { if (swingModel == null) { ensureSliderCreated(); swingModel = new SwingModel(); } return swingModel; } /** * A {@link javax.swing.BoundedRangeModel} for use with {@link RangeBars}. This model can * maps integer values (usually in the range 0 to 100) to floating-point "logical" values. * The method {@link #fireStateChanged(boolean)} is invoked every time the user moved the * slider. * * @version $Id$ * @author Martin Desruisseaux */ private final class SwingModel extends DefaultBoundedRangeModel implements LogicalBoundedRangeModel { /** * Pour compatibilités entre les enregistrements binaires de différentes versions. */ private static final long serialVersionUID = -5691592959010874291L; /** * Valeur minimale. La valeur {@code NaN} indique qu'il * faut puiser le minimum dans les données de {@link RangeBars}. */ private double minimum = Double.NaN; /** * Valeur maximale. La valeur {@code NaN} indique qu'il * faut puiser le maximum dans les données de {@link RangeBars}. */ private double maximum = Double.NaN; /** * Décalage intervenant dans la conversion de la position * de la visière en valeur entière. Le calcul se fait par * <code>int_x=(x-offset)*scale</code>. */ private double offset; /** * Facteur d'échelle intervenant dans la conversion de la position de la visière * en valeur entière. Le calcul se fait par <code>int_x=x*scale+offset</code>. */ private double scale; /** * Indique d'où vient le dernier ajustement * de la valeur: du model ou de la visière. */ private boolean lastAdjustFromModel; /** * La valeur {@code true} indique que {@link #fireStateChanged} * ne doit pas prendre en compte le prochain événement. Ce champ est * utilisé lors des changements de la position de la visière. */ private transient boolean ignoreEvent; /** * Construit un model avec par défaut une plage allant de 0 à 100. Les valeurs * de cette plage sont toujours indépendantes de celles de {@link RangeBars}. */ public SwingModel() { revalidate(); } ////////////////////////////////////////////////////////////////// //////// //////// //////// LogicalBoundedRangeModel interface //////// //////// (not used by this implementation) //////// //////// //////// ////////////////////////////////////////////////////////////////// /** * Spécifie les minimum et maximum des valeurs entières. * Une valeur {@link Double#NaN} signifie de prendre une * valeur par défaut. */ public void setLogicalRange(final double minimum, final double maximum) { this.minimum = minimum; this.maximum = maximum; revalidate(); } /** * Convertit une valeur entière en nombre réel. */ public double toLogical(final int integer) { return (integer-offset)/scale; } /** * Convertit un nombre réel en valeur entière. */ public int toInteger(final double logical) { return (int) Math.round(logical*scale + offset); } ////////////////////////////////////////////////////////////////// //////// //////// //////// Slider position <--> Model value //////// //////// //////// ////////////////////////////////////////////////////////////////// /** * Returns the slider value as an integer in the model range. */ private int getSliderValue() { return (int)Math.round((horizontal ? slider.getMinX() : slider.getMinY()) * scale + offset); } /** * Returns the slider extent as an integer. */ private int getSliderExtent() { return (int)Math.round((horizontal ? slider.getWidth() : slider.getHeight()) * scale); } /** * Met à jour les champs {@link #offset} et {@link #scale}. Les minimum * maximum ainsi que la valeur actuels du model seront réutilisés. C'est * de la responsabilité du programmeur de mettre à jour ces propriétés si * c'est nécessaire. */ private void revalidate() { revalidate(super.getMinimum(), super.getMaximum()); } /** * Update {@link #offset} and {@link #scale} according the supplied model's * {@code lower} and {@code upper} values. It is the caller's * responsability to ensure that {@code lower} and {@code upper} will map the * {@link BoundedRangeModel#getMinimum} and {@link BoundedRangeModel#getMaximum} values. * * @param lower The lower model value. * @param upper The upper model value. */ private void revalidate(final int lower, final int upper) { double minimum = this.minimum; double maximum = this.maximum; try { if (Double.isNaN(minimum)) { final Number min = ConverterRegistry.toNumber(RangeBars.this.getMinimum()); if (min != null) { minimum = min.doubleValue(); } } if (Double.isNaN(maximum)) { final Number max = ConverterRegistry.toNumber(RangeBars.this.getMaximum()); if (max != null) { maximum = max.doubleValue(); } } } catch (ClassNotFoundException exception) { // The minimum or maximum value is not convertible to a number. // Ignore, since the code below will use a default scale. } if (!Double.isNaN(minimum) && !Double.isNaN(maximum)) { scale = (upper-lower)/(maximum-minimum); offset = lower-minimum*scale; } else { scale = 1; offset = 0; } } /** * Synchronize the slider position with this model. If the model has just been adjusted, * then the slider position is updated according. Otherwise, the model is updated * according the current slider position. */ public void synchronize() { if (lastAdjustFromModel) { setSliderPosition(); } else { final int value = getSliderValue(); final int extent = getSliderExtent(); if (value!=super.getValue() || extent!=super.getExtent()) { super.setRangeProperties(value, extent, super.getMinimum(), super.getMaximum(), false); } } } /** * Invoked by {@link RangeBars} when the slider position changed. This method adjust * this model according the current slider position and notifies all registered listeners. */ protected void fireStateChanged(final boolean isAdjusting) { if (!ignoreEvent) { lastAdjustFromModel = false; boolean adjustSlider = false; int lower = super.getMinimum(); int upper = super.getMaximum(); int value = getSliderValue(); int extent = getSliderExtent(); if (value < lower) { final int range = upper-lower; if (extent > range) { extent = range; } value = lower; adjustSlider = true; } else if (value > upper-extent) { final int range = upper-lower; if (extent > range) { extent = range; } value = upper-extent; adjustSlider = true; } super.setRangeProperties(value, extent, lower, upper, isAdjusting); if (adjustSlider) { setSliderPosition(); } } } /** * Modifie la position de la visière en fonction des valeurs actuelles du modèle. */ private void setSliderPosition() { final double min = (super.getValue()-offset)/scale; try { ignoreEvent = true; final double max = min + super.getExtent()/scale; if (horizontal) { slider.setX(min, max); } else { slider.setY(min, max); } } finally { ignoreEvent = false; } repaint(); } /** * Modifie l'ensemble des paramètres d'un coups. */ public void setRangeProperties(final int value, final int extent, final int lower, final int upper, final boolean isAdjusting) { revalidate(lower, upper); lastAdjustFromModel = true; super.setRangeProperties(value, extent, lower, upper, isAdjusting); setSliderPosition(); } /** * Met à jour les champs internes de ce model et lance un * évènement prevenant que la position ou la largeur de la * visière a changée. */ private void setRangeProperties(final int lower, final int upper, final boolean isAdjusting) { revalidate(lower, upper); if (lastAdjustFromModel) { super.setRangeProperties(super.getValue(), super.getExtent(), lower, upper, isAdjusting); setSliderPosition(); } else { super.setRangeProperties(getSliderValue(), getSliderExtent(), lower, upper, isAdjusting); } } /** * Modifie la valeur minimale retournée par {@link #getValue}. * La valeur retournée par cette dernière sera modifiée pour * qu'elle corresponde à la position de la visière dans les * nouvelles limites. */ public void setMinimum(final int minimum) { setRangeProperties(minimum, super.getMaximum(), false); } /** * Modifie la valeur maximale retournée par {@link #getValue}. * La valeur retournée par cette dernière sera modifiée pour * qu'elle corresponde à la position de la visière dans les * nouvelles limites. */ public void setMaximum(final int maximum) { setRangeProperties(super.getMinimum(), maximum, false); } /** * Retourne la position de la visière. */ public int getValue() { if (!lastAdjustFromModel) { super.setValue(getSliderValue()); } return super.getValue(); } /** * Modifie la position de la visière. */ public void setValue(final int value) { lastAdjustFromModel = true; super.setValue(value); setSliderPosition(); } /** * Retourne l'étendu de la visière. */ public int getExtent() { if (!lastAdjustFromModel) { super.setExtent(getSliderExtent()); } return super.getExtent(); } /** * Modifie la largeur de la visière. */ public void setExtent(final int extent) { lastAdjustFromModel = true; super.setExtent(extent); setSliderPosition(); } } /** * Returns a control panel for this {@code RangeBars}. The control * panel may contains buttons, editors and spinners. It make possible * for users to enter exact values in editor fields using the keyboard. * The returned control panel do not contains this {@code RangeBars}: * caller must layout both the control panel and this {@code RangeBars} * (possibly in different windows) if he want to see both of them. * * @param format The format to use for formatting the selected value range, * or {@code null} for a default format. If non-null, * then this format is usually a * {@link java.text.NumberFormat} or a * {@link java.text.DateFormat} instance. * @param minLabel The label to put in front of the editor for * minimum value, or {@code null} if none. * @param maxLabel The label to put in front of the editor for * maximum value, or {@code null} if none. */ private JComponent createControlPanel(Format format, final String minLabel, final String maxLabel) { ensureSliderCreated(); if (format==null) format = axis.getGraduation().getFormat(); final JComponent editor1 = slider.addEditor(format, horizontal ? SwingConstants.WEST : SwingConstants.NORTH, this); final JComponent editor2 = slider.addEditor(format, horizontal ? SwingConstants.EAST : SwingConstants.SOUTH, this); final JComponent panel = new JPanel(new GridBagLayout()); final GridBagConstraints c = new GridBagConstraints(); /* * If the caller supplied labels, add * labels first. Then add the editors. */ c.gridx=0; if (minLabel!=null || maxLabel!=null) { c.anchor = c.EAST; c.gridy=0; panel.add(new JLabel(horizontal ? minLabel : maxLabel), c); c.gridy=1; panel.add(new JLabel(horizontal ? maxLabel : minLabel), c); c.gridx=1; c.insets.left=3; c.anchor = c.CENTER; } c.weightx=1; c.fill=c.HORIZONTAL; c.gridy=0; panel.add(editor1, c); c.gridy=1; panel.add(editor2, c); /* * Adjust focus order. * TODO: this code use deprecated API. */ editor1.setNextFocusableComponent(editor2); editor2.setNextFocusableComponent(this ); this .setNextFocusableComponent(editor1); this .requestFocus(); panel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(), BorderFactory.createEmptyBorder(3,3,3,3))); final Dimension size = panel.getPreferredSize(); size.width = 100; panel.setPreferredSize(size); panel.setMinimumSize (size); return panel; } /** * Returns a new panel with that contains this {@code RangeBars} and * control widgets. Control widgets may include buttons, editors, spinners * and scroll bar. It make possible for users to enter exact values in * editor fields using the keyboard. * * @param format The format to use for formatting the selected value range, * or {@code null} for a default format. If non-null, * then this format is usually a * {@link java.text.NumberFormat} or a * {@link java.text.DateFormat} instance. * @param minLabel The label to put in front of the editor for * minimum value, or {@code null} if none. * @param maxLabel The label to put in front of the editor for * maximum value, or {@code null} if none. */ public JComponent createCombinedPanel(final Format format, final String minLabel, final String maxLabel) { final JComponent panel = new JPanel(new GridBagLayout()); final GridBagConstraints c = new GridBagConstraints(); c.gridx=0; c.weightx=1; c.gridy=0; c.weighty=1; c.fill = c.BOTH; panel.add(horizontal ? this : createScrollPane(), c); if (horizontal) { c.gridx =1; c.weightx=0; c.insets.right = 6; } else { c.gridy =1; c.weighty=0; c.insets.top = 6; } c.fill = c.HORIZONTAL; panel.add(createControlPanel(format, minLabel, maxLabel), c); return panel; } /** * Fait apparaître dans une fenêtre quelques histogrammes * calculés au hasard. Cette méthode sert à vérifier le * bon fonctionnement de la classe {@code RangeBars}. */ public static void main(final String[] args) { int orientation = HORIZONTAL; if (args.length != 0) { final String arg = args[0]; if (arg.equalsIgnoreCase("horizontal")) { orientation = HORIZONTAL; } else if (arg.equalsIgnoreCase("vertical")) { orientation = VERTICAL; } else if (arg.equalsIgnoreCase("vertical2")) { orientation = VERTICAL_EXCEPT_LABELS; } else { System.err.print("Unknow argument: "); System.err.println(arg); return; } } final JFrame frame = new JFrame("RangeBars"); final RangeBars ranges = new RangeBars((Unit)null, orientation); for (int série=1; série<=4; série++) { final String clé="Série #"+série; for (int i=0; i<100; i++) { final double x = 1000*Math.random(); final double w = 30*Math.random(); ranges.addRange(clé, new Double(x), new Double(x+w)); } } ranges.setSelectedRange(12, 38); ranges.setRangeAdjustable(true); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(ranges.createCombinedPanel(null, "Min:", "Max:")); if (ranges.horizontal) { frame.setSize(500, 150); } else { frame.pack(); // For an unknow reason, application freeze here for horizontal bars. } frame.setVisible(true); } }