001/*
002 * Copyright (C) 2012 eXo Platform SAS.
003 *
004 * This is free software; you can redistribute it and/or modify it
005 * under the terms of the GNU Lesser General Public License as
006 * published by the Free Software Foundation; either version 2.1 of
007 * the License, or (at your option) any later version.
008 *
009 * This software is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public
015 * License along with this software; if not, write to the Free
016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
018 */
019
020package org.crsh.text.ui;
021
022import java.util.Arrays;
023
024/**
025 * The layout computes the lengths of a list of contiguous cells.
026 */
027public abstract class Layout {
028
029  public static Layout flow() {
030    return RTL;
031  }
032
033  public static Layout weighted(int... weights) throws NullPointerException, IllegalArgumentException {
034    return new Weighted(weights);
035  }
036
037  /**
038   * Computes the list of lengths for the specifid list of cells with the following constraints:
039   *
040   * <ul>
041   *   <li>the sum of the returned array elements must be equals to the <code>totalLength</code> argument</li>
042   *   <li>a cell length should never be lesser than the provided minimum length</li>
043   * </ul>
044   *
045   * The returned array is the list of lengths from left to right, the array size may be less than the
046   * number of cells (i.e the size of the <code>actualLengths</code> and <code>minLengths</code> arguments). Missing
047   * cells are just be discarded and not part of the resulting layout. Array should contain only positive values,
048   * any zero length cell should be discarded. When cells must be discarded it must begin with the tail of the
049   * list, i.e it is not allowed to discard a cell that does not have a successor.
050   *
051   *
052   * @param spaced true if the cells are separated by one char
053   * @param totalLength the total length of the line
054   * @param actualLengths the actual length : an estimation of what cell's desired length
055   * @param minLengths the minmum length : the length under which a cell cannot be rendered
056   * @return the list of length.
057   */
058  abstract int[] compute(boolean spaced, int totalLength, int[] actualLengths, int[] minLengths);
059
060  public static class Weighted extends Layout {
061
062    /** The weights. */
063    private final int[] weights;
064
065    /**
066     * Create a new weighted layout.
067     *
068     * @param weights the weights
069     * @throws NullPointerException if the weights argument is null
070     * @throws IllegalArgumentException if any weight is negative
071     */
072    private Weighted(int... weights) throws NullPointerException, IllegalArgumentException {
073      if (weights == null) {
074        throw new NullPointerException("No null weights accepted");
075      }
076      for (int weight : weights) {
077        if (weight < 0) {
078          throw new IllegalArgumentException("No negative weight accepted");
079        }
080      }
081      this.weights = weights.clone();
082    }
083
084    public int[] getWeights() {
085      return weights.clone();
086    }
087
088    @Override
089    int[] compute(boolean spaced, int length, int[] actualLengths, int[] minLengths) {
090
091      //
092      int count = Math.min(actualLengths.length, weights.length);
093
094      //
095      for (int i = count;i > 0;i--) {
096
097        //
098        int totalLength = length;
099        int totalWeight = 0;
100        for (int j = 0;j < i;j++) {
101          totalWeight += weights[j];
102          if (spaced) {
103            if (j > 0) {
104              totalLength--;
105            }
106          }
107        }
108
109        // Compute the length of each cell
110        int[] ret = new int[i];
111        for (int j = 0;j < i;j++) {
112          int w = totalLength * weights[j];
113          if (w < minLengths[j] * totalWeight) {
114            ret = null;
115            break;
116          } else {
117            ret[j] = w;
118          }
119        }
120
121        //
122        if (ret != null) {
123          // Error based scaling inspired from Brensenham algorithm:
124          // => sum of the weights == length
125          // => minimize error
126          // for instance with "foo","bar" scaled to 11 chars
127          // using floor + division gives "foobar_____"
128          // this methods gives           "foo_bar____"
129          int err = 0;
130          for (int j = 0;j < ret.length;j++) {
131
132            // Compute base value
133            int value = ret[j] / totalWeight;
134
135            // Lower value
136            int lower = value * totalWeight;
137            int errLower = err + ret[j] - lower;
138
139            // Upper value
140            int upper = lower + totalWeight;
141            int errUpper = err + ret[j] - upper;
142
143            // We choose between lower/upper according to the accumulated error
144            // and we propagate the error
145            if (Math.abs(errLower) < Math.abs(errUpper)) {
146              ret[j] = value;
147              err = errLower;
148            } else {
149              ret[j] = value + 1;
150              err = errUpper;
151            }
152          }
153          return ret;
154        }
155      }
156
157      //
158      return null;
159    }
160  }
161
162  private static final Layout RTL = new Layout() {
163
164    @Override
165    int[] compute(boolean spaced, int length, int[] actualLengths, int[] minLengths) {
166
167      //
168      int[] ret = actualLengths.clone();
169
170      //
171      int totalLength = 0;
172      for (int i = 0;i < actualLengths.length;i++) {
173        totalLength += actualLengths[i];
174        if (spaced && i > 0) {
175          totalLength++;
176        }
177      }
178
179      //
180      int index = minLengths.length - 1;
181      while (totalLength > length && index >= 0) {
182        int delta = totalLength - length;
183        int bar = actualLengths[index] - minLengths[index];
184        if (delta <= bar) {
185          // We are done
186          totalLength = length;
187          ret[index] -= delta;
188        } else {
189          int foo = actualLengths[index];
190          if (spaced) {
191            if (index > 0) {
192              foo++;
193            }
194          }
195          totalLength -= foo;
196          ret[index] = 0;
197          index--;
198        }
199      }
200
201      //
202      if (totalLength > 0) {
203        if (index == minLengths.length - 1) {
204          return ret;
205        } else {
206          return Arrays.copyOf(ret, index + 1);
207        }
208      } else {
209        return null;
210      }
211    }
212  };
213/*
214
215  public static final ColumnLayout DISTRIBUTED = new ColumnLayout() {
216    @Override
217    public int[] compute(Border border, int width, int[] widths, int[] minWidths) {
218      int index = 0;
219      while (true) {
220
221        // Compute now the number of chars
222        boolean done = false;
223        int total = 0;
224        for (int i = 0;i < widths.length;i++) {
225          if (widths[i] >= minWidths[i]) {
226            total += widths[i];
227            if (border != null) {
228              if (done) {
229                total++;
230              }
231              else {
232                total += 2;
233                done = true;
234              }
235            }
236          }
237        }
238
239        // It's not valid
240        if (total == 0) {
241          return null;
242        }
243
244        //
245        int delta = width - total;
246
247        //
248        if (delta == 0) {
249          break;
250        } else if (delta > 0) {
251          switch (widths[index]) {
252            case 0:
253              break;
254            default:
255              widths[index]++;
256              break;
257          }
258          index = (index + 1) % widths.length;
259        } else {
260
261          // First try to remove from a column above min size
262          int found = -1;
263          for (int i = widths.length - 1;i >= 0;i--) {
264            int p = (index + i) % widths.length;
265            if (widths[p] > minWidths[p]) {
266              found = p;
267              if (--index < 0) {
268                index += widths.length;
269              }
270              break;
271            }
272          }
273
274          // If we haven't found a victim then we consider removing a column
275          if (found == -1) {
276            for (int i = widths.length - 1;i >= 0;i--) {
277              if (widths[i] > 0) {
278                found = i;
279                break;
280              }
281            }
282          }
283
284          // We couldn't find any solution we give up
285          if (found == -1) {
286            break;
287          } else {
288            widths[found]--;
289          }
290        }
291      }
292
293      //
294      return widths;
295    }
296  };
297*/
298}