Java tutorial
/* * Copyright (c) 2013 Philippe VIENNE * * This file is a part of SpeleoGraph * * SpeleoGraph is free software: you can redistribute * it and/or modify it under the terms of the GNU General * Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your * option) any later version. * * SpeleoGraph is distributed in the hope that it will * be useful, but WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public * License along with SpeleoGraph. * If not, see <http://www.gnu.org/licenses/>. */ package org.cds06.speleograph.data; import org.apache.commons.lang3.Validate; import org.cds06.speleograph.GraphPanel; import org.cds06.speleograph.I18nSupport; import org.cds06.speleograph.graph.DrawStyle; import org.cds06.speleograph.utils.Modification; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jfree.chart.LegendItem; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.Plot; import org.jfree.chart.renderer.xy.HighLowRenderer; import org.jfree.chart.renderer.xy.XYAreaRenderer; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.DomainOrder; import org.jfree.data.general.DatasetChangeEvent; import org.jfree.data.general.DatasetChangeListener; import org.jfree.data.general.DatasetGroup; import org.jfree.data.time.DateRange; import org.jfree.data.xy.OHLCDataset; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.*; import java.io.File; import java.util.*; import java.util.List; /** * Represent a Series of Data. * A series is coherent set of Data. */ public class Series implements Comparable, OHLCDataset, Cloneable { /** * Logger for debug and errors in Series instances. */ @SuppressWarnings("UnusedDeclaration") @NonNls private static final Logger log = LoggerFactory.getLogger(Series.class); private static GraphPanel graphPanel; private boolean stepped = false; private boolean minMax = false; /** * Create a new Series opened from a file with a default Type. * * @param origin The file where this series has been read. * @param type The type for this series */ public Series(@NotNull File origin, @NotNull Type type) { Validate.notNull(type, "Type can not be null");// NON-NLS Validate.notNull(origin); this.origin = origin; Series.lastOpenedFile = origin; this.type = type; this.itemsName = "Initialisation"; instances.add(this); setStyle(DrawStyle.AUTO); notifyListeners(); } /** * Flag to define if we must show this series on chart. */ private boolean show = false; /** * The file where this series has been read. */ private File origin = null; /** * The last opened file, independent of the current series. */ private static File lastOpenedFile = null; /** * Series items, children of series. */ private ArrayList<Item> items = new ArrayList<>(); /** * Are the current items linked to others ? (modification on more than one series cancelled, for example) */ private boolean applyToAll = false; /** * Series items list name. */ private String itemsName; /** * Series previous modifications, just undone. */ private ArrayList<Modification> previousModifs = new ArrayList<>(MAX_UNDO_ITEMS); /** * Series next modifications, waiting to be redone. */ private ArrayList<Modification> nextModifs = new ArrayList<>(MAX_UNDO_ITEMS); /** * The number of modification that can be canceled. */ public static final int MAX_UNDO_ITEMS = 10; /** * The name of the series. */ private String name; /** * Axis linked to this series. * This axis replaces the Type's Axis only if it's not null. */ private NumberAxis axis = null; private static final ArrayList<Series> instances = new ArrayList<>(20); /** * Get all series currently in the SpeleoGraph Instance * * @return Unmodifiable list of instances. */ public static List<Series> getInstances() { return Collections.unmodifiableList(instances); } /** * Detect if series is the first element of instances list. * * @return true if it's the first element. */ public boolean isFirst() { return instances.indexOf(this) == 0; } /** * Detect if series is the last element of instances list. * * @return true if it's the last element. */ public boolean isLast() { return instances.indexOf(this) == instances.size() - 1; } private double seriesMaxValue; private double seriesMinValue; /** * Move the current series to n-1 position. */ public void upSeriesInList() { int index = instances.indexOf(this), newIndex = index - 1; if (newIndex < 0) return; // We are already on top. Series buffer = instances.get(newIndex); instances.set(newIndex, instances.get(index)); instances.set(index, buffer); notifyListeners(); } /** * Move the current series to n+1 position. */ public void downSeriesInList() { int index = instances.indexOf(this), newIndex = index + 1; if (newIndex >= instances.size()) return; // We are already on top. Series buffer = instances.get(newIndex); instances.set(newIndex, instances.get(index)); instances.set(index, buffer); notifyListeners(); } public static void setGraphPanel(GraphPanel graphPanel) { Series.graphPanel = graphPanel; } /** * Count the number of items into this Series. * * @return The number of items (assuming is 0 or more) */ public int getItemCount() { if (items == null) return 0; return items.size(); } /** * Get the file used to read the data. * * @return The data origin's file. */ public File getOrigin() { return origin; } public double getSeriesMaxValue() { return seriesMaxValue; } public double getSeriesMinValue() { return seriesMinValue; } /** * Compute the date range of the items in this set. * * @return A date range which contains the lower and upper bounds of data. */ public DateRange getRange() { int max = getItemCount(); DateRange range; if (max == 0) { Date now = Calendar.getInstance().getTime(); return new DateRange(now, now); } Date minDate = new Date(Long.MAX_VALUE), maxDate = new Date(Long.MIN_VALUE); for (int i = 0; i < max; i++) { Item item = items.get(i); if (item.getDate().before(minDate)) minDate = item.getDate(); if (item.getDate().after(maxDate)) maxDate = item.getDate(); } range = new DateRange(minDate, maxDate); return range; } /** * Returns the last opened file, independent of the current series. * @return the last opened file, independent of the current series. */ public static File getLastOpenedFile() { return lastOpenedFile; } /** * Getter for the series Type. * If this series is not attached to a DataSet, then we suppose that Type is {@link Type#UNKNOWN} * * @return The type for this series */ @NotNull public Type getType() { return type; } /** * Say if we should show this series on a graph. * * @return true if we should show this series. */ public boolean isShow() { return show; } /** * Set if we should show this series on a graph. * * @param v true if we should show */ public void setShow(boolean v) { show = v; notifyListeners(); } /** * Getter for the axis to display for this series. * If the series does not define his own axis, this function will search the Type's Axis. * * @return A NumberAxis to display it in a chart (never null) * @throws IllegalStateException if we can not find an axis for this series. */ public NumberAxis getAxis() { if (axis != null) return axis; else if (type != null) return getType().getAxis(); else throw new IllegalStateException("Can not find an axis for series !"); //NON-NLS } /** * Setter for the axis. * If an axis is set to the series, then the Type axis would not be shown and the Chart will display this axis for * the series even if other shown series are using the Type's axis. * * @param axis The axis to set for this series. */ public void setAxis(NumberAxis axis) { if (axis == null) { log.info("Setting a null axis to series " + getName()); } this.axis = axis; notifyInstanceListeners(); } /** * Listeners for this series. */ private ArrayList<DatasetChangeListener> listeners = new ArrayList<>(); /** * Notify listeners about something changed into the series. */ public void notifyListeners() { final DatasetChangeEvent event = new DatasetChangeEvent(this, this); if (graphPanel != null) graphPanel.datasetChanged(event); for (DatasetChangeListener listener : staticListeners) { listener.datasetChanged(event); } for (DatasetChangeListener listener : listeners) { listener.datasetChanged(event); } } /** * Add a listener on series' properties. * * @param listener The listener will be called on events */ public void addChangeListener(DatasetChangeListener listener) { if (!listeners.contains(listener)) listeners.add(listener); } /** * Remove a listener on series' properties. * * @param listener The listener which will be removed. */ public void removeChangeListener(DatasetChangeListener listener) { if (listeners.contains(listener)) listeners.remove(listener); } /** * Getter for the human name of this series. * If the name is not set, it computes a name as "[Origin File Name] - [Name of the Type]" * * @return The display name for this Series. */ public String getName() { return name == null ? getOrigin().getName() + " - " + getType().getName() : name; } /** * Say if the Series name has been chosen by human or generated. * * @return true if name is set by human */ public boolean isNameHumanSet() { return name != null; } /** * Set an human name for this Series. * * @param name The name to set (should not be null) */ public void setName(String name) { this.name = name; notifyListeners(); } @Override public int compareTo(@NotNull Object o) { return this.equals(o) ? 0 : -1; } /** * Add an item to this series. * * @param item The item to add. */ public void add(Item item) { Validate.notNull(item); items.add(item); if (item.getValue() > seriesMaxValue) seriesMaxValue = item.getValue(); else if (item.getValue() < seriesMinValue) seriesMinValue = item.getValue(); } /** * Returns the high-value for the specified series and item. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The value. */ @Override public Number getHigh(int series, int item) { return getHighValue(series, item); } /** * Returns the high-value (as a double primitive) for an item within a * series. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The high-value. */ @Override public double getHighValue(int series, int item) { if (isMinMax()) if (isShow() && (item > -1 && item < items.size())) return items.get(item).getHigh(); else return Double.NaN; else return Double.NaN; } /** * Returns the low-value for the specified series and item. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The value. */ @Override public Number getLow(int series, int item) { return getLowValue(series, item); } /** * Returns the low-value (as a double primitive) for an item within a * series. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The low-value. */ @Override public double getLowValue(int series, int item) { if (isMinMax()) if (isShow() && (item > -1 && item < items.size())) return items.get(item).getLow(); else return Double.NaN; else return Double.NaN; } /** * Returns the open-value for the specified series and item. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The value. */ @Override public Number getOpen(int series, int item) { return getOpenValue(series, item); } /** * Returns the open-value (as a double primitive) for an item within a * series. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The open-value. */ @Override public double getOpenValue(int series, int item) { return Double.NaN; } /** * Returns the y-value for the specified series and item. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The value. */ @Override public Number getClose(int series, int item) { return getCloseValue(series, item); } /** * Returns the close-value (as a double primitive) for an item within a * series. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The close-value. */ @Override public double getCloseValue(int series, int item) { return Double.NaN; } /** * Returns the volume for the specified series and item. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The value. */ @Override public Number getVolume(int series, int item) { return getVolumeValue(series, item); } /** * Returns the volume-value (as a double primitive) for an item within a * series. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The volume-value. */ @Override public double getVolumeValue(int series, int item) { return Double.NaN; } /** * Returns the order of the domain (or X) values returned by the dataset. * * @return The order (never <code>null</code>). */ @Override public DomainOrder getDomainOrder() { return DomainOrder.ASCENDING; } /** * Returns the number of items in a series. * <br><br> * It is recommended that classes that implement this method should throw * an <code>IllegalArgumentException</code> if the <code>series</code> * argument is outside the specified range. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @return The item count. */ @Override public int getItemCount(int series) { return isShow() ? items.size() : 0; } /** * Returns the x-value for an item within a series. The x-values may or * may not be returned in ascending order, that is up to the class * implementing the interface. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (in the range <code>0</code> to * <code>getItemCount(series)</code>). * @return The x-value (never <code>null</code>). */ @Override public Number getX(int series, int item) { return getXValue(series, item); } /** * Returns the x-value for an item within a series. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (in the range <code>0</code> to * <code>getItemCount(series)</code>). * @return The x-value. */ @Override public double getXValue(int series, int item) { try { if (isShow() && (item > -1 && item < items.size())) return items.get(item).getDate().getTime(); else return Double.NaN; } catch (NullPointerException e) { return Double.NaN; } } /** * Returns the y-value for an item within a series. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (in the range <code>0</code> to * <code>getItemCount(series)</code>). * @return The y-value (possibly <code>null</code>). */ @Override public Number getY(int series, int item) { return getYValue(series, item); } /** * Returns the y-value (as a double primitive) for an item within a series. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (in the range <code>0</code> to * <code>getItemCount(series)</code>). * @return The y-value. */ @Override public double getYValue(int series, int item) { if (isShow() && (item > -1 && item < items.size())) return items.get(item).getValue(); else return Double.NaN; } /** * Returns the number of series in the dataset. * * @return The series count. */ @Override public int getSeriesCount() { return isShow() ? 1 : 0; } /** * Returns the key for a series. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @return The key for the series. */ @Override public Comparable getSeriesKey(int series) { return getName(); } /** * Returns the index of the series with the specified key, or -1 if there * is no such series in the dataset. * * @param seriesKey the series key (<code>null</code> permitted). * @return The index, or -1. */ @Override public int indexOf(Comparable seriesKey) { return getName().compareTo(String.valueOf(seriesKey)) == 0 ? 0 : -1; } public void setType(Type type) { this.type = type; } private Type type = Type.UNKNOWN; /** * Returns the dataset group. * * @return The dataset group. */ @Override public Type getGroup() { return getType(); } /** * Sets the dataset group. * * @param group the dataset group. */ @Override public void setGroup(DatasetGroup group) { if (group instanceof Type) { setType((Type) group); } else { throw new IllegalArgumentException("Group must by a SpeleoGraph Type"); } } /** * Define the {@link Object#toString()} to be alias of {@link #getName()}. * * @return The name of this series. * @see #getName() */ @Override public String toString() { return getName(); } /** * Has the ability to return an html version of the {@link #toString()} containing more info. * @param b Want to get html version ? * @return The html version of the {@link #toString()} method containing min/max values and unit. */ public String toString(boolean b) { if (b) return "<em>" + this.getName() + "</em> - [" + this.getSeriesMinValue() + " --> " + this.getSeriesMaxValue() + "] " + this.getType().getUnit(); else return toString(); } public void delete() { instances.remove(this); items.clear(); notifyListeners(); } private XYItemRenderer renderer; public XYItemRenderer getRenderer() { if (renderer == null) setupRendererAuto(); if (color != null) { renderer.setSeriesPaint(0, color); } return renderer; } //TODO Write doc public Series generateSampledSeries(long length) { final Series newSeries = new Series(origin, Type.WATER); newSeries.setStepped(true); final int itemsCount = getItemCount(); final ArrayList<Item> newItems = newSeries.items; double bufferValue = 0D; DateRange range = getRange(); long lastStartBuffer = range.getLowerMillis(); newItems.add(new Item(newSeries, new Date(lastStartBuffer), 0.0)); for (int i = 1; i < itemsCount; i++) { final Item originalItem = items.get(i), previousOriginalItem = items.get(i - 1); if (lastStartBuffer + length <= originalItem.getDate().getTime()) { newItems.add(new Item(newSeries, new Date(lastStartBuffer), bufferValue)); newItems.add(new Item(newSeries, new Date(lastStartBuffer + length), bufferValue)); final long time = originalItem.getDate().getTime(); lastStartBuffer = lastStartBuffer + length; if (lastStartBuffer + 2 * length < time) { newItems.add(new Item(newSeries, new Date(lastStartBuffer), 0)); lastStartBuffer = time - ((originalItem.getDate().getTime() - lastStartBuffer) % length); newItems.add(new Item(newSeries, new Date(lastStartBuffer), 0)); } bufferValue = 0D; } bufferValue = bufferValue + (originalItem.getValue() - previousOriginalItem.getValue()); } newItems.add(new Item(newSeries, new Date(lastStartBuffer), bufferValue)); newItems.add(new Item(newSeries, new Date(range.getUpperMillis()), bufferValue)); return newSeries; } private static final HashSet<DatasetChangeListener> staticListeners = new HashSet<>(2); public static void addListener(DatasetChangeListener listener) { staticListeners.add(listener); } public List<Item> getItems() { return Collections.unmodifiableList(items); } public String getItemsName() { return itemsName; } private DrawStyle style = DrawStyle.AUTO; public DrawStyle getStyle() { return style; } public void setStyle(DrawStyle style) { Validate.notNull(style); if (isMinMax() && !(style == DrawStyle.AUTO || style == DrawStyle.HIGH_LOW)) return; if (style.equals(this.style)) return; this.style = style; switch (style) { case AUTO: setupRendererAuto(); break; case AREA: renderer = new NewAreaRenderer(XYAreaRenderer.AREA); break; case HIGH_LOW: renderer = new NewHighLowRenderer(); break; default: case LINE: renderer = new NewLineAndShapeRenderer(true, false); } notifyListeners(); } private void setupRendererAuto() { if (isMinMax()) { renderer = new NewHighLowRenderer(); } else if (isStepped()) { renderer = new NewAreaRenderer(XYAreaRenderer.AREA); } else { renderer = new NewLineAndShapeRenderer(true, false); } } /** * Used to override the display of the legend */ private static class NewLineAndShapeRenderer extends XYLineAndShapeRenderer { public NewLineAndShapeRenderer(boolean a, boolean b) { super(a, b); } @Override public LegendItem getLegendItem(int datasetIndex, int series) { LegendItem legend = super.getLegendItem(datasetIndex, series); return new LegendItem(legend.getLabel(), legend.getDescription(), legend.getToolTipText(), legend.getURLText(), Plot.DEFAULT_LEGEND_ITEM_BOX, legend.getFillPaint()); } } /** * Used to override the display of the legend */ private static class NewAreaRenderer extends XYAreaRenderer { public NewAreaRenderer(int a) { super(a); } @Override public LegendItem getLegendItem(int datasetIndex, int series) { LegendItem legend = super.getLegendItem(datasetIndex, series); return new LegendItem(legend.getLabel(), legend.getDescription(), legend.getToolTipText(), legend.getURLText(), Plot.DEFAULT_LEGEND_ITEM_BOX, legend.getFillPaint()); } } /** * Used to override the display of the legend */ private static class NewHighLowRenderer extends HighLowRenderer { public NewHighLowRenderer() { super(); } @Override public LegendItem getLegendItem(int datasetIndex, int series) { LegendItem legend = super.getLegendItem(datasetIndex, series); return new LegendItem(legend.getLabel(), legend.getDescription(), legend.getToolTipText(), legend.getURLText(), Plot.DEFAULT_LEGEND_ITEM_BOX, legend.getFillPaint()); } } /** * Color of the series on screen. */ private Color color; public Color getColor() { if (color == null && renderer != null) { return (Color) renderer.getSeriesPaint(0); } return color; } public void setColor(Color color) { if (renderer == null) setupRendererAuto(); this.color = color; notifyListeners(); } /** * Notify all static listeners that an edit occurs. * <p>Note: This function will refresh graphics, so it could occur thread blocking</p> */ public static void notifyInstanceListeners() { final DatasetChangeEvent event = new DatasetChangeEvent(Series.class, null); if (graphPanel != null) graphPanel.datasetChanged(event); for (DatasetChangeListener listener : staticListeners) { listener.datasetChanged(event); } } public boolean hasOwnAxis() { return axis != null; } public void setStepped(boolean stepped) { this.stepped = stepped; } public boolean isStepped() { return stepped; } public void setMinMax(boolean minMax) { this.minMax = minMax; } public boolean isMinMax() { return minMax; } public boolean isWater() { return this.getType().getName().equals(Type.WATER.getName()); } public boolean isWaterCumul() { return this.getType().getName().equals(Type.WATER_CUMUL.getName()); } public boolean isPressure() { return this.getType().getName().equals(Type.PRESSURE.getName()); } @SuppressWarnings("UnusedDeclaration") public boolean isTemperature() { return this.getType().getName().equals(Type.TEMPERATURE.getName()); } @SuppressWarnings("UnusedDeclaration") public boolean isWaterHeight() { return this.getType().getName().equals(Type.WATER_HEIGHT.getName()); } /** * Remplace les donnes de la srie par celles d'une sous-srie. * Par exemple : On a des donnes du 25/07 au 30/07, on veut uniquement les donnes du 28 7h au 28 9h. * @param start Date de dbut. * @param end Date de fin. * @param applyToAll Is the modification applied to more than one series ? */ public void subSeries(Date start, Date end, boolean applyToAll) { this.setItems(extractSubSerie(start, end), I18nSupport.translate("actions.limit"), applyToAll); notifyListeners(); } /** * Extraire une sous-srie de donnes. * Par exemple : On a des donnes du 25/07 au 30/07, on veut extraire les donnes du 28 7h au 28 9h. * @param start Date de dbut. * @param end Date de fin. * @return The {@link java.util.ArrayList} containing all the items that match the date range. */ public ArrayList<Item> extractSubSerie(Date start, Date end) { ArrayList<Item> newItems = new ArrayList<>(items.size()); for (Item i : items) { if (i.getDate().after(start) && i.getDate().before(end)) newItems.add(i); } return newItems; } /** * Setter for items, stores the old items in a field so they can be retrieved by {@link #undo()}. * The stored list of changes is limited to ten items. * This method also clears the redo list. * @param items The {@link ArrayList} to set in place of the existing one. * @param name The name of the modification that occurred. */ public void setItems(ArrayList<Item> items, String name) { setItems(items, name, false); } /** * Setter for items, stores the old items in a field so they can be retrieved by {@link #undo()}. * The stored list of changes is limited to ten items. * This method also clears the redo list. * @param items The {@link ArrayList} to set in place of the existing one. * @param name The name of the modification that occurred. * @param applyToAll Is the modification applied to more than one series ? */ public void setItems(ArrayList<Item> items, String name, boolean applyToAll) { Modification m = new Modification(this.itemsName, new Date(), this.items, this, applyToAll); this.applyToAll = applyToAll; this.previousModifs.add(m); this.nextModifs.clear(); Modification.clearRedoList(); Modification.addToUndoList(m); this.items = items; setMinMaxValue(); this.itemsName = name; if (this.previousModifs.size() > MAX_UNDO_ITEMS) this.previousModifs.remove(0); notifyListeners(); } private void setMinMaxValue() { seriesMaxValue = Collections.max(items).getValue(); seriesMinValue = Collections.min(items).getValue(); } /** * Undo the last destructive action (done through {@link #setItems(java.util.ArrayList, java.lang.String)} done on the series. * Can only undo ten items. * All undone actions can be retrieved using {@link #redo()}. * @return true if the undo could have been done, else false. */ public boolean undo() { if (!this.canUndo()) return false; final int previousModifsSize = this.previousModifs.size(); Modification m = this.createModif(); this.nextModifs.add(m); Modification.addToRedoList(m); Modification old = this.previousModifs.get(previousModifsSize - 1); this.items = old.getItems(); setMinMaxValue(); this.applyToAll = old.isApplyToAll(); this.previousModifs.remove(previousModifsSize - 1); Modification.removeLastUndo(); notifyListeners(); return true; } /** * Reset the series (undo everything). * @return true if the reset could have been done, else false. */ public boolean reset() { if (!this.canUndo()) return false; while (this.canUndo()) undo(); return true; } /** * Redo the last undone action (see {@link #undo()}) on the series. * Can only redo things that have been undone. * @return true if the redo could have been done, else false. */ public boolean redo() { if (!this.canRedo()) return false; final int nextModifsSize = this.nextModifs.size(); Modification m = this.createModif(); this.previousModifs.add(m); Modification.addToUndoList(m); Modification next = this.nextModifs.get(nextModifsSize - 1); this.items = next.getItems(); setMinMaxValue(); this.applyToAll = next.isApplyToAll(); this.nextModifs.remove(nextModifsSize - 1); Modification.removeLastRedo(); notifyListeners(); return true; } /** * Tells if a series can undo something. * @return true if an undo can be done, else false. */ public boolean canUndo() { return this.previousModifs.size() > 0; } /** * Tells if a series can redo something. * @return true if an redo can be done, else false. */ public boolean canRedo() { return this.nextModifs.size() > 0; } /** * Create a {@link org.cds06.speleograph.utils.Modification} with current data (items, items name and current date). * @return the new {@link org.cds06.speleograph.utils.Modification} */ public Modification createModif() { return new Modification(this.itemsName, new Date(), this.items, this, this.applyToAll); } public String getLastUndoName() { if (!canUndo()) return "Pas de modification annuler"; return getLastModif().getName(); } public Modification getLastModif() { if (canUndo()) return this.previousModifs.get(previousModifs.size() - 1); return null; } public String getNextRedoName() { if (!canRedo()) return "Pas de modification refaire"; return getNextRedo().getName(); } public Modification getNextRedo() { if (canRedo()) return this.nextModifs.get(nextModifs.size() - 1); return null; } }