blob: 4ccfde8d44e706dce1c7c9e971aaad379a0f67d7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2013, 2014 Original authors and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Original authors and others - initial API and implementation
* Dirk Fauth <dirk.fauth@googlemail.com> - Added percentage sizing
* Dirk Fauth <dirk.fauth@googlemail.com> - Added scaling
* neal zhang <nujiah001@126.com> - change some methods and fields visibility
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.layer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;
import org.eclipse.nebula.widgets.nattable.persistence.IPersistable;
/**
* This class stores the size configuration of rows/columns within the NatTable.
*
* Mixed mode (fixed/percentage sizing):<br>
* The mixed mode is only working if percentage sizing is enabled globally, and
* the fixed sized positions are marked separately.
*/
public class SizeConfig implements IPersistable {
public static final String PERSISTENCE_KEY_DEFAULT_SIZE = ".defaultSize"; //$NON-NLS-1$
public static final String PERSISTENCE_KEY_DEFAULT_SIZES = ".defaultSizes"; //$NON-NLS-1$
public static final String PERSISTENCE_KEY_SIZES = ".sizes"; //$NON-NLS-1$
public static final String PERSISTENCE_KEY_RESIZABLE_BY_DEFAULT = ".resizableByDefault"; //$NON-NLS-1$
public static final String PERSISTENCE_KEY_RESIZABLE_INDEXES = ".resizableIndexes"; //$NON-NLS-1$
public static final String PERSISTENCE_KEY_PERCENTAGE_SIZING = ".percentageSizing"; //$NON-NLS-1$
public static final String PERSISTENCE_KEY_PERCENTAGE_SIZING_INDEXES = ".percentageSizingIndexes"; //$NON-NLS-1$
/**
* The global default size of this {@link SizeConfig}.
*/
protected int defaultSize;
/**
* Map that contains default sizes per column.
*/
protected final Map<Integer, Integer> defaultSizeMap = new TreeMap<Integer, Integer>();
/**
* Map that contains sizes per column.
*/
protected final Map<Integer, Integer> sizeMap = new TreeMap<Integer, Integer>();
/**
* Map that contains the resizable information per row/column.
*/
protected final Map<Integer, Boolean> resizablesMap = new TreeMap<Integer, Boolean>();
/**
* The global resizable information of this {@link SizeConfig}.
*/
protected boolean resizableByDefault = true;
/**
* Map that contains the percentage sizing information per row/column.
*/
protected final Map<Integer, Boolean> percentageSizingMap = new TreeMap<Integer, Boolean>();
/**
* Flag to tell whether the sizing is done for pixel or percentage values.
*/
protected boolean percentageSizing = false;
/**
* The available space needed for percentage calculation on resizing.
*/
protected int availableSpace = -1;
/**
* Map that contains the real pixel size. Will only be used on percentage
* sizing. This map is not persisted as it will be calculated on resize.
*/
protected final Map<Integer, Integer> realSizeMap = new TreeMap<Integer, Integer>();
/**
* Map that contains the cached aggregated sizes.
*/
protected final Map<Integer, Integer> aggregatedSizeCacheMap = new HashMap<Integer, Integer>();
/**
* Flag that indicates if the aggregated size cache is valid or if it needs
* to get recalculated.
*/
protected boolean isAggregatedSizeCacheValid = true;
/**
* The {@link IDpiConverter} that is used for scaling DPI conversion.
*/
protected IDpiConverter dpiConverter;
/**
* Create a new {@link SizeConfig} with the given default size.
*
* @param defaultSize
* The default size to use.
*/
public SizeConfig(int defaultSize) {
this.defaultSize = defaultSize;
}
// Persistence
@Override
public void saveState(String prefix, Properties properties) {
properties.put(prefix + PERSISTENCE_KEY_DEFAULT_SIZE, String.valueOf(this.defaultSize));
saveMap(this.defaultSizeMap, prefix + PERSISTENCE_KEY_DEFAULT_SIZES, properties);
saveMap(this.sizeMap, prefix + PERSISTENCE_KEY_SIZES, properties);
properties.put(prefix + PERSISTENCE_KEY_RESIZABLE_BY_DEFAULT, String.valueOf(this.resizableByDefault));
saveMap(this.resizablesMap, prefix + PERSISTENCE_KEY_RESIZABLE_INDEXES, properties);
properties.put(prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING, String.valueOf(this.percentageSizing));
saveMap(this.percentageSizingMap, prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING_INDEXES, properties);
}
private void saveMap(Map<Integer, ?> map, String key, Properties properties) {
if (map.size() > 0) {
StringBuilder strBuilder = new StringBuilder();
for (Integer index : map.keySet()) {
strBuilder.append(index);
strBuilder.append(':');
strBuilder.append(map.get(index));
strBuilder.append(',');
}
properties.setProperty(key, strBuilder.toString());
}
}
@Override
public void loadState(String prefix, Properties properties) {
// ensure to cleanup the current states prior loading new ones
this.defaultSizeMap.clear();
this.sizeMap.clear();
this.resizablesMap.clear();
this.aggregatedSizeCacheMap.clear();
String persistedDefaultSize = properties.getProperty(prefix + PERSISTENCE_KEY_DEFAULT_SIZE);
if (persistedDefaultSize != null && persistedDefaultSize.length() > 0) {
this.defaultSize = Integer.valueOf(persistedDefaultSize).intValue();
}
String persistedResizableDefault = properties.getProperty(prefix + PERSISTENCE_KEY_RESIZABLE_BY_DEFAULT);
if (persistedResizableDefault != null && persistedResizableDefault.length() > 0) {
this.resizableByDefault = Boolean.valueOf(persistedResizableDefault).booleanValue();
}
String persistedPercentageSizing = properties.getProperty(prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING);
if (persistedPercentageSizing != null && persistedPercentageSizing.length() > 0) {
setPercentageSizing(Boolean.valueOf(persistedPercentageSizing).booleanValue());
}
loadBooleanMap(prefix + PERSISTENCE_KEY_RESIZABLE_INDEXES, properties, this.resizablesMap);
loadIntegerMap(prefix + PERSISTENCE_KEY_DEFAULT_SIZES, properties, this.defaultSizeMap);
loadIntegerMap(prefix + PERSISTENCE_KEY_SIZES, properties, this.sizeMap);
loadBooleanMap(prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING_INDEXES, properties, this.percentageSizingMap);
}
private void loadIntegerMap(String key, Properties properties, Map<Integer, Integer> map) {
String property = properties.getProperty(key);
if (property != null) {
map.clear();
StringTokenizer tok = new StringTokenizer(property, ","); //$NON-NLS-1$
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
int separatorIndex = token.indexOf(':');
map.put(Integer.valueOf(token.substring(0, separatorIndex)),
Integer.valueOf(token.substring(separatorIndex + 1)));
}
}
}
private void loadBooleanMap(String key, Properties properties, Map<Integer, Boolean> map) {
String property = properties.getProperty(key);
if (property != null) {
StringTokenizer tok = new StringTokenizer(property, ","); //$NON-NLS-1$
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
int separatorIndex = token.indexOf(':');
map.put(Integer.valueOf(token.substring(0, separatorIndex)),
Boolean.valueOf(token.substring(separatorIndex + 1)));
}
}
}
// Default size
/**
* Set the default size that should be used in case there is no position
* based size configured.
*
* @param size
* The default size to set.
*/
public void setDefaultSize(int size) {
if (size < 0) {
throw new IllegalArgumentException("size < 0"); //$NON-NLS-1$
}
this.defaultSize = size;
this.isAggregatedSizeCacheValid = false;
}
/**
* @return The default size that is used in case there is no position based
* size configured.
*/
public int getDefaultSize() {
return upScale(this.defaultSize);
}
public void setDefaultSize(int position, int size) {
if (this.defaultSize < 0) {
throw new IllegalArgumentException("size < 0"); //$NON-NLS-1$
}
this.defaultSizeMap.put(position, size);
this.isAggregatedSizeCacheValid = false;
}
private int getDefaultSize(int position) {
Integer size = this.defaultSizeMap.get(position);
if (size != null) {
return size.intValue();
} else {
return this.defaultSize;
}
}
// Size
public int getAggregateSize(int position) {
if (position < 0) {
return -1;
} else if (position == 0) {
return 0;
} else if (isAllPositionsSameSize() && !isPercentageSizing()) {
// if percentage sizing is used, the sizes in defaultSize are used
// as percentage values and not as pixel values, therefore another
// value needs to be considered
return upScale(position * this.defaultSize);
} else {
// See if the cache is valid, if not clear it.
if (!this.isAggregatedSizeCacheValid) {
this.aggregatedSizeCacheMap.clear();
this.isAggregatedSizeCacheValid = true;
}
if (!this.aggregatedSizeCacheMap.containsKey(position)) {
int aggregatedSize = calculateAggregatedSize(position);
this.aggregatedSizeCacheMap.put(position, aggregatedSize);
}
return this.aggregatedSizeCacheMap.get(position);
}
}
public int getSize(int position) {
Integer size = null;
if (isPercentageSizing()) {
size = this.realSizeMap.get(position);
} else {
if (this.sizeMap.containsKey(position)) {
size = upScale(this.sizeMap.get(position));
}
}
if (size != null) {
return size.intValue();
} else {
return upScale(getDefaultSize(position));
}
}
/**
* Sets the given size for the given position. This method can be called
* manually for configuration via {@link DataLayer} and will be called on
* resizing within the rendered UI. This is why there is a check for
* percentage configuration. If this {@link SizeConfig} is configured to not
* use percentage sizing, the size is taken as is. If percentage sizing is
* enabled, the given size will be calculated to percentage value based on
* the already known pixel values.
* <p>
* If you want to use percentage sizing you should use
* {@link SizeConfig#setPercentage(int, int)} for manual size configuration
* to avoid unnecessary calculations.
*
* @param position
* The position for which the size should be set.
* @param size
* The size in pixels to set for the given position.
*/
public void setSize(int position, int size) {
if (size < 0) {
throw new IllegalArgumentException("size < 0"); //$NON-NLS-1$
}
if (isPositionResizable(position)) {
// check whether the given value should be remembered as is or if it
// needs to be calculated
if (!isPercentageSizing(position)) {
this.sizeMap.put(position, downScale(size));
} else {
if (this.availableSpace > 0) {
Double percentage = ((double) size * 100) / this.availableSpace;
Integer oldValue = this.sizeMap.get(position);
int diff = percentage.intValue();
if (oldValue != null) {
diff = diff - oldValue;
} else {
// there was no percentage value before
// we need to calculate the before value out of the
// realSizeMap otherwise the resizing effect would
// have strange effects
if (this.realSizeMap.containsKey(position)) {
Double calculated = ((double) this.realSizeMap.get(position) * 100) / this.availableSpace;
diff = diff - calculated.intValue();
}
}
this.sizeMap.put(position, percentage.intValue());
// check the adjacent positions for percentage corrections
int nextPosition = position + 1;
while (diff != 0 && this.realSizeMap.containsKey(nextPosition)) {
diff = updateAdjacentPosition(nextPosition, diff);
nextPosition++;
}
int previousPosition = position - 1;
while (diff != 0
&& this.realSizeMap.containsKey(previousPosition)) {
diff = updateAdjacentPosition(previousPosition, diff);
previousPosition--;
}
if (diff != 0 && oldValue == null) {
// if the diff is not 0 and there was no size value set
// before we will remove the prior set value again
// this is because the position was configured as the
// only percentage sizing position with no specified
// value, which technically means that it should always
// take the remaining space
this.sizeMap.remove(position);
}
}
}
calculatePercentages(this.availableSpace, this.realSizeMap.size());
this.isAggregatedSizeCacheValid = false;
}
}
/**
* This method is used for resizing in percentage mode. If a position that
* is configured for percentage sizing is resized, the size diff needs to be
* added/removed from adjacent cells, to ensure consistent size calculation.
*
* @param position
* The position to update.
* @param diff
* The diff that should be applied to the position
* @return The remaining diff or 0 if the diff could be completely applied
* to the position.
*/
private int updateAdjacentPosition(int position, int diff) {
if (this.sizeMap.containsKey(position) || this.realSizeMap.containsKey(position)) {
if (isPercentageSizing(position)) {
if (this.sizeMap.containsKey(position)) {
// there is a follow-up position that is configured for
// percentage sizing
// and there is value specified for that position
int currentValue = this.sizeMap.get(position);
if (diff < currentValue) {
this.sizeMap.put(position, currentValue - diff);
return 0;
} else {
diff = diff - (currentValue + 1);
// never accept a percentage value < 1 as then the
// position would disappear
this.sizeMap.put(position, 1);
}
}
return 0;
}
}
return diff;
}
/**
* Will set the given percentage size information for the given position and
* will set the given position to be sized via percentage value.
*
* @param position
* The positions whose percentage sizing information should be
* set.
* @param percentage
* The percentage value to set, always dependent on the available
* space for percentage sizing, which can be less than the real
* available space in case there are also positions that are
* configured for fixed size.
*/
public void setPercentage(int position, int percentage) {
if (percentage < 0) {
throw new IllegalArgumentException("percentage < 0"); //$NON-NLS-1$
}
if (isPositionResizable(position)) {
this.percentageSizingMap.put(position, Boolean.TRUE);
this.sizeMap.put(position, percentage);
this.realSizeMap.put(position, calculatePercentageValue(percentage, this.availableSpace));
calculatePercentages(this.availableSpace, this.realSizeMap.size());
}
}
// Resizable
/**
* @return The global resizable information of this {@link SizeConfig}.
*/
public boolean isResizableByDefault() {
return this.resizableByDefault;
}
/**
* Checks if there is a special resizable configuration for the given
* position. If not the global resizable information is returned.
*
* @param position
* The position of the row/column for which the resizable
* information is requested.
* @return <code>true</code> if the given row/column position is resizable,
* <code>false</code> if not.
*/
public boolean isPositionResizable(int position) {
Boolean resizable = this.resizablesMap.get(position);
if (resizable != null) {
return resizable.booleanValue();
}
return this.resizableByDefault;
}
/**
* Sets the resizable configuration for the given row/column position.
*
* @param position
* The position of the row/column for which the resizable
* configuration should be set.
* @param resizable
* <code>true</code> if the given row/column position should be
* resizable, <code>false</code> if not.
*/
public void setPositionResizable(int position, boolean resizable) {
this.resizablesMap.put(position, resizable);
}
/**
* Sets the global resizable configuration. Will reset all special resizable
* configurations.
*
* @param resizableByDefault
* <code>true</code> if all rows/columns should be resizable,
* <code>false</code> if no row/column should be resizable.
*/
public void setResizableByDefault(boolean resizableByDefault) {
this.resizablesMap.clear();
this.resizableByDefault = resizableByDefault;
}
// All positions same size
public boolean isAllPositionsSameSize() {
return this.defaultSizeMap.size() == 0 && this.sizeMap.size() == 0;
}
/**
* @return <code>true</code> if the size of at least one position is
* interpreted in percentage, <code>false</code> if the size of all
* positions is interpreted by pixel.
*/
public boolean isPercentageSizing() {
if (!this.percentageSizingMap.isEmpty()) {
for (Boolean pSize : this.percentageSizingMap.values()) {
if (pSize)
return true;
}
}
return this.percentageSizing;
}
/**
* @param percentageSizing
* <code>true</code> if the size of the positions should be
* interpreted percentaged, <code>false</code> if the size of the
* positions should be interpreted by pixel.
*/
public void setPercentageSizing(boolean percentageSizing) {
this.percentageSizing = percentageSizing;
this.isAggregatedSizeCacheValid = false;
}
/**
* Checks if there is a special percentage sizing configuration for the
* given position. If not the global percentage sizing information is
* returned.
*
* @param position
* The position of the row/column for which the percentage sizing
* information is requested.
* @return <code>true</code> if the given row/column position is sized by
* percentage value, <code>false</code> if not.
*/
public boolean isPercentageSizing(int position) {
Boolean percentageSizing = this.percentageSizingMap.get(position);
if (percentageSizing != null) {
return percentageSizing;
}
return this.percentageSizing;
}
/**
* Sets the percentage sizing configuration for the given row/column
* position.
*
* @param position
* The position of the row/column for which the percentage sizing
* configuration should be set.
* @param percentageSizing
* <code>true</code> if the given row/column position should be
* interpreted in percentage, <code>false</code> if not.
*/
public void setPercentageSizing(int position, boolean percentageSizing) {
this.percentageSizingMap.put(position, percentageSizing);
this.isAggregatedSizeCacheValid = false;
}
/**
* Will calculate the real pixel values for the positions if percentage
* sizing is enabled.
*
* @param space
* The space that is available for rendering.
* @param positionCount
* The number of positions that should be handled by this
* {@link SizeConfig}
*/
public void calculatePercentages(int space, int positionCount) {
if (isPercentageSizing()) {
this.isAggregatedSizeCacheValid = false;
this.availableSpace = space;
int percentageSpace = calculateAvailableSpace(space);
int sum = 0;
int real = 0;
int realSum = 0;
int fixedSum = 0;
List<Integer> noInfoPositions = new ArrayList<Integer>();
Integer positionValue = null;
for (int i = 0; i < positionCount; i++) {
positionValue = this.sizeMap.get(i);
if (positionValue != null) {
if (isPercentageSizing(i)) {
sum += positionValue;
real = calculatePercentageValue(positionValue,
percentageSpace);
} else {
real = positionValue;
fixedSum += real;
}
realSum += real;
this.realSizeMap.put(i, real);
} else {
// remember the position for which no size information
// exists needed to calculate the size for those positions
// dependent on the remaining space
noInfoPositions.add(i);
}
}
int[] correction = correctPercentageValues(sum, positionCount);
if (correction != null) {
sum = correction[0];
realSum = correction[1] + fixedSum;
}
if (!noInfoPositions.isEmpty()) {
// now calculate the size for the remaining columns
double remaining = new Double(space) - realSum;
Double remainingColSpace = remaining / noInfoPositions.size();
for (Integer position : noInfoPositions) {
sum += (remainingColSpace / space) * 100;
this.realSizeMap.put(position, remainingColSpace.intValue());
}
// If there are positions for which no size information exist,
// the size config will use 100 percent of the available space
// on percentage sizing. To handle rounding issues just set the
// sum to 100 for correct calculation results.
sum = 100;
}
if (sum == 100) {
// check if the sum of the calculated values is the same as the
// given space if not distribute the missing pixels to some of
// the other columns this is needed because of rounding issues
// on 100% with odd-numbered pixel values
int valueSum = 0;
int lastPos = -1;
for (Map.Entry<Integer, Integer> entry : this.realSizeMap.entrySet()) {
valueSum += entry.getValue();
lastPos = Math.max(lastPos, entry.getKey());
}
if (space > 0 && valueSum < space) {
// distribute the missing pixels
int missingPixels = (space - valueSum);
int pos = 0;
for (int i = missingPixels; i > 0; i--) {
if (!this.realSizeMap.containsKey(pos)) {
// there are more missing pixels than columns
// start over at position 0
pos = 0;
}
int posValue = this.realSizeMap.get(pos);
this.realSizeMap.put(pos, posValue + 1);
pos++;
}
}
}
}
}
/**
* @param percentage
* The percentage value.
* @param space
* The available space
* @return The percentage value of the given space.
*/
private int calculatePercentageValue(int percentage, int space) {
double factor = (double) percentage / 100;
return new Double(space * factor).intValue();
}
/**
* Calculates the available space for percentage size calculation. This is
* necessary to support mixed mode of sizing, e.g. if two columns are
* configured to have fixed size of 50 pixels and one column that should
* take the rest of the available space of 500 pixels, the available space
* for percentage sizing is 400 pixels.
*
* @param space
* The whole available space for rendering.
* @return The available space for percentage sizing. Might be negative if
* the width of all fixed sized positions is greater than the
* available space.
*/
protected int calculateAvailableSpace(int space) {
if (!this.percentageSizingMap.isEmpty()) {
if (this.percentageSizing) {
for (Map.Entry<Integer, Boolean> entry : this.percentageSizingMap
.entrySet()) {
if (!entry.getValue()) {
if (this.sizeMap.containsKey(entry.getKey()))
space -= this.sizeMap.get(entry.getKey());
}
}
}
}
return space;
}
/**
* This method is used to correct the calculated percentage values in case a
* user configured more than 100 percent. In that case the set percentage
* values are scaled down to not exceed 100 percent.
*
* @param sum
* The sum of all configured percentage sized positions.
* @param positionCount
* The number of positions to check.
* @return Integer array with the sum value at first position and the new
* calculated real pixel sum at second position in case a
* corrections took place. Will return <code>null</code> in case no
* correction happened.
*/
protected int[] correctPercentageValues(int sum, int positionCount) {
Map<Integer, Integer> toModify = new TreeMap<Integer, Integer>();
for (int i = 0; i < positionCount; i++) {
Integer positionValue = this.sizeMap.get(i);
if (positionValue != null && isPercentageSizing(i)) {
toModify.put(i, this.realSizeMap.get(i));
}
}
// if the sum is greater than 100 we need to normalize the percentage
// values
if (sum > 100) {
// calculate the factor which needs to be used to normalize the
// values
double factor = Double.valueOf(100) / Double.valueOf(sum);
// update the percentage size values by the calculated factor
int realSum = 0;
for (Map.Entry<Integer, Integer> mod : toModify.entrySet()) {
int oldValue = mod.getValue();
int newValue = Double.valueOf(oldValue * factor).intValue();
realSum += newValue;
this.realSizeMap.put(mod.getKey(), newValue);
}
return new int[] { 100, realSum };
}
// the given sum is not greater than 100 so we do not have to modify
// anything
return null;
}
private int calculateAggregatedSize(int position) {
int resizeAggregate = 0;
int resizedColumns = 0;
Map<Integer, Integer> mapToUse = isPercentageSizing() ? this.realSizeMap : this.sizeMap;
for (Integer resizedPosition : mapToUse.keySet()) {
if (resizedPosition.intValue() < position) {
resizedColumns++;
resizeAggregate += mapToUse.get(resizedPosition);
} else {
break;
}
}
// also take into account the default size configuration per position
for (Integer defaultPosition : this.defaultSizeMap.keySet()) {
if (defaultPosition.intValue() < position) {
if (!mapToUse.containsKey(defaultPosition)) {
resizedColumns++;
resizeAggregate += this.defaultSizeMap.get(defaultPosition).intValue();
}
} else {
break;
}
}
int result = (position * this.defaultSize) + resizeAggregate - (resizedColumns * this.defaultSize);
return isPercentageSizing() ? result : upScale(result);
}
/**
* Recalculate the percentage values for the given amount of columns. Needed
* for structural changes that aren't caused by a client are resize, e.g.
* adding a column.
*
* @param positionCount
* The number of columns that should be used to calculate the
* percentage values.
*/
public void updatePercentageValues(int positionCount) {
calculatePercentages(this.availableSpace, positionCount);
}
/**
* Calculates the size value dependent on a possible configured scaling from
* pixel to DPI value.
*
* @param value
* The value that should be up scaled.
* @return The scaled value if a {@link IDpiConverter} is configured, the
* value itself if no {@link IDpiConverter} is set.
*
* @see IDpiConverter#convertPixelToDpi(int)
*/
protected int upScale(int value) {
if (this.dpiConverter == null) {
return value;
}
return this.dpiConverter.convertPixelToDpi(value);
}
/**
* Calculates the size value dependent on a possible configured scaling from
* DPI to pixel value.
*
* <p>
* This method is used for percentage sizing calculations.
* </p>
*
* @param value
* The value that should be down scaled.
* @return The scaled value if a {@link IDpiConverter} is configured, the
* value itself if no {@link IDpiConverter} is set.
*/
protected int downScale(int value) {
if (this.dpiConverter == null) {
return value;
}
return this.dpiConverter.convertDpiToPixel(value);
}
/**
*
* @param dpiConverter
* The {@link IDpiConverter} to use for size scaling.
*/
public void setDpiConverter(IDpiConverter dpiConverter) {
this.dpiConverter = dpiConverter;
}
}