/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. */ package org.geotoolkit.image.io.mosaic; import java.net.URI; import java.net.URL; import java.io.File; import java.io.Serializable; import java.awt.Rectangle; import javax.imageio.spi.ImageReaderSpi; import org.geotoolkit.resources.Errors; /** * Formats the filename of tiles to be created by {@link MosaicBuilder}. * * @author Cédric Briançon (Geomatys) * @author Martin Desruisseaux (Geomatys) * @version 3.00 * * @since 2.5 * @module */ final class FilenameFormatter implements Serializable { /** * For cross-version inter-operability. */ private static final long serialVersionUID = 67144666748146079L; /** * Default value for {@link #prefix}. Current implementation uses "L" as in "Level". */ private static final String DEFAULT_PREFIX = "L"; /** * The expected size of level, row and column fields in generated names. */ private int levelFieldSize, rowFieldSize, columnFieldSize; /** * The prefix to put before tile filenames. If {@code null}, will be inferred * from the source filename. */ private String prefix; /** * The separator between level number and the (row,column) coordinates. */ private String levelSeparator; /** * The separator between column and row. */ private String locationSeparator; /** * The suffix, typically the file extension with its leading dot. */ private String suffix; /** * Creates a default filename formatter. */ public FilenameFormatter() { levelSeparator = "_"; locationSeparator = ""; } /** * Initializes the formatter with a suffix inferred from the given image reader provider * if none was explicitly set. The longest file extension is chosen (e.g. {@code "tiff"} * instead of {@code "tif"}). * * @param tileReaderSpi The image reader provider to be used for reading tiles. */ public void initialize(final ImageReaderSpi tileReaderSpi) { if (prefix == null) { prefix = DEFAULT_PREFIX; } suffix = ""; final String[] suffixes = tileReaderSpi.getFileSuffixes(); if (suffixes != null) { for (final String s : suffixes) { if (s.length() > suffix.length()) { suffix = s; } } if (!suffix.startsWith(".")) { suffix = "." + suffix; } } levelFieldSize = 0; rowFieldSize = 0; columnFieldSize = 0; } /** * Computes the value for {@link #levelFieldSize}. * It will be used by {@link #generateFilename}. * * @param n The expected number of overviews. */ public void computeLevelFieldSize(final int n) { levelFieldSize = ((int) Math.log10(n)) + 1; } /** * Computes the values for {@link #columnFieldSize} and {@link #rowFieldSize}. * They will be used by {@link #generateFilename}. */ public void computeFieldSizes(final Rectangle imageBounds, final Rectangle tileBounds) { final StringBuilder buffer = new StringBuilder(); format26(buffer, imageBounds.width / tileBounds.width, 0); columnFieldSize = buffer.length(); buffer.setLength(0); format10(buffer, imageBounds.height / tileBounds.height, 0); rowFieldSize = buffer.length(); } /** * Formats a number in base 10. * * @param buffer The buffer where to write the row number. * @param n The row number to format, starting at 0. * @param size The expected width (for padding with '0'). */ private static void format10(final StringBuilder buffer, final int n, final int size) { final String s = Integer.toString(n + 1); for (int i=size-s.length(); --i >= 0;) { buffer.append('0'); } buffer.append(s); } /** * Formats a column in base 26. For example the first column is {@code 'A'}. If there is * more columns than alphabet has letters, then another letter is added in the same way. * * @param buffer The buffer where to write the column number. * @param n The column number to format, starting at 0. * @param size The expected width (for padding with 'A'). */ private static void format26(final StringBuilder buffer, int n, final int size) { if (size > 1 || n >= 26) { format26(buffer, n / 26, size - 1); n %= 26; } buffer.append((char) ('A' + n)); } /** * If the {@linkplain #prefix} is not already set, build a default value from the given input. * * @param input The image input, typically as a {@link File} or an other {@link TileManager}. */ public void ensurePrefixSet(final Object input) { if (prefix == null) { String filename; if (input instanceof File) { filename = ((File) input).getName(); } else if (input instanceof URI || input instanceof URL || input instanceof CharSequence) { filename = input.toString(); filename = filename.substring(filename.lastIndexOf('/') + 1); } else { filename = DEFAULT_PREFIX; } int length = filename.lastIndexOf('.'); if (length < 0) { length = filename.length(); } int i; for (i=0; i<length; i++) { if (!Character.isLetter(filename.charAt(i))) { break; } } prefix = filename.substring(0, i); } } /** * Generates a filename for the current tile based on the position of this tile in the raster. * For example, a tile in the first overview level, which is localized on the 5th column and * 2nd row may have a name like "{@code L1_E2.png}". * * @param level The level of overview. First level is 0. * @param column The index of columns. First column is 0. * @param row The index of rows. First row is 0. * @return A filename based on the position of the tile in the whole raster. */ public String generateFilename(final int level, final int column, final int row) { final StringBuilder buffer = new StringBuilder(prefix); format10(buffer, level, levelFieldSize); buffer.append(levelSeparator); format26(buffer, column, columnFieldSize); buffer.append(locationSeparator); format10(buffer, row, rowFieldSize); if (suffix != null) { buffer.append(suffix); } return buffer.toString(); } /** * Returns a pattern for the given filename. For example if the overview level is 2, * the column is 3 and the row is 4 (numbered from 0), and if the given filename is * {@code "L3_AD05.png"}, then the returned pattern is * {@code "L{level:1}_{column:2}{row:2}.png"} * <p> * The state of this formatter is modified by this method. * * @param level The level of overview. First level is 0. * @param column The index of columns. First column is 0. * @param row The index of rows. First row is 0. * @param filename The filename. * @return A pattern for the given filename, or {@code null} if the pattern can not be found. */ public String guessPattern(final int level, final int column, final int row, final String filename) { /* * Extracts immediately the file extension, if any. Then we will scan the filename * in reverse order, because we want to search for numbers aligned to the right. */ int last = filename.length(); final StringBuilder buffer = new StringBuilder(); loop: for (int fieldNumber=0; ;fieldNumber++) { final int fieldValue; final boolean useLetters; switch (fieldNumber) { case 0: fieldValue = row; useLetters = false; break; case 1: fieldValue = column; useLetters = true; break; case 2: fieldValue = level; useLetters = false; break; default: break loop; } buffer.setLength(0); if (useLetters) { format26(buffer, fieldValue, 0); } else { format10(buffer, fieldValue, 0); } /* * For a given field (row, column or level), searches the last occurrence of this * field in the filename, starting just before the field processed in the previous loop * iteration (if any). If the field is not found, or if it not bounded by a different * character than the ones used for formatting the field value (digits or letters), * then this method stops immediately and returns null. */ final String fieldText = buffer.toString(); int length = fieldText.length(); int start = filename.lastIndexOf(fieldText, last - length); if (start < 0) { return null; } final int end = start + length; if (end < last) { final char c = filename.charAt(end); if (useLetters ? Character.isLetter(c) : Character.isDigit(c)) { return null; } } final char fill = useLetters ? 'A' : '0'; while (start != 0) { final char c = filename.charAt(start - 1); if (c != fill) { if (useLetters ? Character.isLetter(c) : Character.isDigit(c)) { return null; // The number is not the one given in argument to this method. } break; // We have found the beginning of the number (base 10 or 26). } start--; } /* * Sets every formatter fields with the values found so far. */ final String separator = filename.substring(end, last); length = end - start; switch (fieldNumber) { case 0: suffix = separator; rowFieldSize = length; break; case 1: locationSeparator = separator; columnFieldSize = length; break; case 2: levelSeparator = separator; levelFieldSize = length; break; } last = start; } prefix = filename.substring(0, last); assert filename.equals(generateFilename(level, column, row)) : filename; return toString(); } /** * Applies the given pattern to this formatter. * * @param pattern The pattern. * @throws IllegalArgumentException if the pattern is not recognized. */ public void applyPattern(final String pattern) throws IllegalArgumentException { int last = 0; RuntimeException cause = null; // In case of failure. for (int fieldNumber=0; ;fieldNumber++) { final String field; switch (fieldNumber) { case 0: field = "{level:"; break; case 1: field = "{column:"; break; case 2: field = "{row:"; break; default: { suffix = pattern.substring(last); return; // Everything done, no exception. } } int i = pattern.indexOf(field, last); if (i < 0) { break; // Exception will be thrown outside the loop. } final String separator = pattern.substring(last, i); i += field.length(); last = pattern.indexOf('}', i); if (last < 0) { break; // Exception will be thrown outside the loop. } final int n; try { n = Integer.parseInt(pattern.substring(i, last)); } catch (NumberFormatException e) { cause = e; break; // Exception will be thrown outside the loop. } last++; switch (fieldNumber) { case 0: prefix = separator; levelFieldSize = n; break; case 1: levelSeparator = separator; columnFieldSize = n; break; case 2: locationSeparator = separator; rowFieldSize = n; break; } } throw new IllegalArgumentException(Errors.format( Errors.Keys.IllegalArgument_2, "pattern", pattern), cause); } /** * Returns the pattern. */ @Override public String toString() { return (prefix != null ? prefix : DEFAULT_PREFIX) + "{level:" + levelFieldSize + '}' + levelSeparator + "{column:" + columnFieldSize + '}' + locationSeparator + "{row:" + rowFieldSize + '}' + suffix; } }