blob: bc0e6b8293b14fdbde429934292e02728fff1565 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.sis.referencing.operation.gridded;
import java.util.Collection;
import java.util.Optional;
import java.io.Writer;
import java.io.StringWriter;
import java.io.Serializable;
import java.io.IOException;
import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import javax.imageio.ImageReader; // For javadoc
import org.opengis.metadata.spatial.PixelOrientation;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Classes;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.io.TableAppender;
import org.apache.sis.referencing.privy.AffineTransform2D;
/**
* A tile identified by a location, a dimension and a subsampling.
* This class can be used for constructing a mosaic or a pyramid of images.
* While the Javadoc discusses image I/O operations, this {@code Tile} class is not restricted to imagery.
* This class is also used for managing tiles in a datum shift file encoded in NTv2 format.
*
* <p>Each tile contains the following:</p>
* <ul class="verbose">
* <li><b>A format name or a provider of {@link ImageReader}</b> (optional).
* The same format is typically used for every tiles, but this is not mandatory.
* An {@linkplain ImageReader image reader} can be instantiated before a tile is read.</li>
*
* <li><b>An image input</b> (optional), typically a {@link java.nio.file.Path} or {@link java.net.URL}.
* The input is often different for every tile to be read, but this is not mandatory. For example, tiles
* could be stored at different {@linkplain #getImageIndex() image index} in the same file.</li>
*
* <li><b>An image index</b> to be given to {@link ImageReader#read(int)} for reading the tile.
* This index is often 0.</li>
*
* <li><b>The upper-left corner</b> in the destination image as a {@link Point},
* or the upper-left corner together with the image size as a {@link Rectangle}.
* If the upper-left corner has been given as a point, then the
* {@linkplain ImageReader#getWidth(int) width} and {@linkplain ImageReader#getHeight(int) height}
* may be obtained from the image reader when first needed, which may have a slight performance cost.
* If the upper-left corner has been given as a rectangle instead, then this performance cost is avoided
* but the user is responsible for the accuracy of the information provided.
*
* <div class="note"><b>Note:</b>
* the upper-left corner is the {@linkplain #getLocation() location} of this tile in the
* {@linkplain javax.imageio.ImageReadParam#setDestination destination image} when no
* {@linkplain javax.imageio.ImageReadParam#setDestinationOffset destination offset} are specified.
* If the user specified a destination offset, then the tile location will be translated accordingly
* for the image being read.
* </div></li>
*
* <li><b>The subsampling relative to the tile having the best resolution.</b>
* This is not the subsampling to apply when reading this tile, but rather the subsampling that we would
* need to apply on the tile having the finest resolution in order to produce an image equivalent to this tile.
* The subsampling is (1,1) for the tile having the finest resolution, (2,3) for an overview having
* half the width and third of the height for the same geographic extent, <i>etc.</i>
* (note that overviews are not required to have the same geographic extent - the above is just an example).
*
* <div class="note"><b>Note 1:</b>
* the semantic assumes that overviews are produced by subsampling, not by interpolation or pixel averaging.
* The latter are not prohibited, but doing so introduce some subsampling-dependent variations in images read,
* which would not be what we would expect from a strictly compliant {@link ImageReader}.</div>
*
* <div class="note"><b>Note 2:</b>
* tile {@linkplain #getLocation() location} and {@linkplain #getRegion() region} coordinates should be
* specified in the overview pixel units - they should <em>not</em> be pre-multiplied by subsampling.
* This multiplication should be performed automatically by a {@code TileManager} when comparing regions
* from tiles at different subsampling levels.
* </div></li>
* </ul>
*
* The tiles are not required to be arranged on a regular grid, but performances may be better if they are.
* {@link TileOrganizer} is responsible for analyzing the layout of a collection of tiles.
*
* <h2>Multi-threading</h2>
* This class is thread-safe. In addition {@code Tile} instances can be considered as immutable after construction.
* However, some properties may be available only after the tiles have been processed by a {@link TileOrganizer},
* or only after {@link #fetchSize()} has been invoked.
*
* @author Martin Desruisseaux (Geomatys)
*
* @see org.apache.sis.storage.tiling.Tile
*/
public class Tile implements Serializable {
/**
* For cross-version compatibility during serialization.
*/
private static final long serialVersionUID = 1638238437701248681L;
/**
* The upper-left corner in the mosaic (destination image). Should be considered as final,
* since this class is supposed to be mostly immutable. However, the value can be changed
* by {@link #translate(int, int)} before the {@code Tile} instance is made public.
*
* @see #getLocation()
* @see #getRegion()
*/
private int x, y;
/**
* The size of the tile, or 0 if not yet computed.
*
* @see #getSize()
* @see #getRegion()
*/
private int width, height;
/**
* The subsampling relative to the tile having the finest resolution. If this tile is the one with
* finest resolution, then the value shall be 1. Should never be 0 or negative, except if the value
* has not yet been computed.
*
* <p>This field should be considered as final. It is not final only because
* {@link TileOrganizer} may compute this value automatically.</p>
*
* @see #getSubsampling()
*/
private int xSubsampling, ySubsampling;
/**
* The "grid to real world" transform, used by {@link TileOrganizer} in order to compute
* the {@linkplain #getRegion() region} for this tile. This field is set to {@code null} when
* {@link TileOrganizer}'s work is in progress, and set to a new value on completion.
*
* <p><b>Note:</b> {@link TileOrganizer} really needs a new instance for each tile.
* No caching allowed before {@link TileOrganizer} processing.
* Caching is allowed <em>after</em> {@link TileOrganizer} processing is completed.</p>
*/
private AffineTransform gridToCRS;
/**
* Creates a tile for the given tile location. This constructor can be used when the size of
* the tile is unknown. This tile size will be fetched automatically by {@link #fetchSize()}
* when {@link #getSize()} or {@link #getRegion()} is invoked for the first time.
*
* @param location the upper-left corner in the mosaic (destination image).
* @param subsampling the subsampling relative to the tile having the finest resolution,
* or {@code null} if none. If non-null, width and height shall be strictly positive.
* This argument can be understood as pixel size relative to finest resolution.
*/
public Tile(final Point location, final Dimension subsampling) {
x = location.x;
y = location.y;
setSubsampling(subsampling);
}
/**
* Creates a tile for the given region. This constructor should be used when the size of the tile is known.
* This information avoid the cost of fetching the size when {@link #getSize()} or {@link #getRegion()} is
* first invoked.
*
* @param region the region (location and size) in the mosaic (destination image).
* @param subsampling the subsampling relative to the tile having the finest resolution,
* or {@code null} if none. If non-null, width and height shall be strictly positive.
* This argument can be understood as pixel size relative to finest resolution.
* @throws IllegalArgumentException if the given region {@linkplain Rectangle#isEmpty() is empty}.
*/
public Tile(final Rectangle region, final Dimension subsampling) {
x = region.x;
y = region.y;
width = region.width;
height = region.height;
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "region"));
}
setSubsampling(subsampling);
}
/**
* Creates a tile for the given region and <q>grid to real world</q> transform.
* This constructor can be used when the {@linkplain #getLocation() location} of the tile is unknown.
* The location and subsampling will be computed automatically when this tile will be processed by a
* {@link TileOrganizer}.
*
* <p>When using this constructor, the {@link #getLocation()}, {@link #getRegion()} and
* {@link #getSubsampling()} methods will throw an {@link IllegalStateException} until
* this tile has been processed by a {@link TileOrganizer}, which will compute those
* values automatically.</p>
*
* @param region the tile region, or {@code null} if unknown.
* The (<var>x</var>,<var>y</var> location of this region is typically (0,0).
* The final location will be computed when this tile will be given to a {@link TileOrganizer}.
* @param gridToCRS the <q>grid to real world</q> transform mapping pixel
* {@linkplain PixelOrientation#UPPER_LEFT upper left} corner.
*/
public Tile(final Rectangle region, final AffineTransform gridToCRS) {
ArgumentChecks.ensureNonNull("gridToCRS", gridToCRS);
if (region != null) {
x = region.x;
y = region.y;
width = Math.max(region.width, 0); // Empty region authorized.
height = Math.max(region.height, 0);
}
this.gridToCRS = new AffineTransform(gridToCRS); // Really need a new instance - no cache
}
/**
* Creates a new tile for the given final transform.
* This is used for storing {@link TileOrganizer} results.
*/
Tile(final AffineTransform gridToCRS, final Rectangle region) {
this.x = region.x;
this.y = region.y;
this.width = region.width;
this.height = region.height;
this.gridToCRS = gridToCRS; // Should be an AffineTransform2D instance.
setSubsampling(null);
}
/**
* Checks if the location, region, and subsampling can be returned. Throws an exception if this
* tile has been created without location and not yet processed by {@link TileOrganizer}.
*/
private void ensureDefined() throws IllegalStateException {
if (xSubsampling == 0 || ySubsampling == 0) {
throw new IllegalStateException();
}
}
/**
* Returns the tile upper-left corner coordinates in the mosaic.
*
* @return the tile upper-left corner.
* @throws IllegalStateException if this tile has been {@linkplain #Tile(Rectangle, AffineTransform)
* created without location} and has not yet been processed by {@link TileOrganizer}.
*
* @see javax.imageio.ImageReadParam#setDestinationOffset(Point)
*/
public synchronized Point getLocation() throws IllegalStateException {
ensureDefined();
return new Point(x, y);
}
/**
* Returns the image size. If this tile has been created with the {@linkplain #Tile(Rectangle, Dimension)
* constructor expecting a rectangle}, then the dimension of that rectangle is returned.
* Otherwise {@link #fetchSize()} is invoked and the result is cached for future usage.
*
* <p>At the difference of {@link #getLocation()} and {@link #getRegion()}, this method never
* throw {@link IllegalStateException} because the tile size does not depend on the processing
* performed by {@link TileOrganizer}.</p>
*
* @return the tile size.
* @throws IOException if an I/O operation was required for fetching the tile size and that operation failed.
* @throws IllegalStateException if this class does not have sufficient information for providing a tile size.
*/
public synchronized Dimension getSize() throws IOException {
// No call to ensureDefined().
if ((width | height) == 0) {
final Dimension size = fetchSize();
width = size.width;
height = size.height;
}
return new Dimension(width, height);
}
/**
* Invoked when the tile size need to be read or computed. The default implementation throws
* {@link IllegalStateException} since this base class has no information for computing a tile size.
* Subclasses can override and, for example, get the size with {@link ImageReader#getWidth(int)} and
* {@link ImageReader#getHeight(int)}.
*
* @return the tile size.
* @throws IOException if an I/O operation was required for fetching the tile size and that operation failed.
* @throws IllegalStateException if this class does not have sufficient information for providing a tile size.
*/
protected Dimension fetchSize() throws IOException {
throw new IllegalStateException();
}
/**
* Returns the upper-left corner location in the mosaic together with the tile size.
* If this tile has been created with the {@linkplain #Tile(Rectangle, Dimension)
* constructor expecting a rectangle}, a copy of the specified rectangle is returned.
* Otherwise {@link #fetchSize()} is invoked and the result is cached for future usage.
*
* @return the region in the mosaic (destination image).
* @throws IOException if an I/O operation was required for fetching the tile size and that operation failed.
* @throws IllegalStateException if this tile has been {@linkplain #Tile(Rectangle, AffineTransform) created
* without location} and has not yet been processed by {@link TileOrganizer}, of if this tile does
* not have enough information for providing a tile size.
*
* @see javax.imageio.ImageReadParam#setSourceRegion(Rectangle)
*/
public synchronized Rectangle getRegion() throws IllegalStateException, IOException {
ensureDefined();
if ((width | height) == 0) {
final Dimension size = fetchSize();
width = size.width;
height = size.height;
}
return new Rectangle(x, y, width, height);
}
/**
* Returns the {@linkplain #getRegion() region} multiplied by the subsampling.
* This is this tile coordinates in the units of the tile having the finest resolution,
* as opposed to other methods which are always in units relative to this tile.
*
* @return the region in units relative to the tile having the finest resolution.
* @throws IOException if an I/O operation was required for fetching the tile size and that operation failed.
* @throws ArithmeticException if the region exceeded the capacity of 32-bits integer type.
* @throws IllegalStateException if this tile has been {@linkplain #Tile(Rectangle, AffineTransform) created
* without location} and has not yet been processed by {@link TileOrganizer}, of if this tile does
* not have enough information for providing a tile size.
*/
public Rectangle getRegionOnFinestLevel() throws IOException {
final Rectangle region;
final int sx, sy;
synchronized (this) {
region = getRegion();
sx = xSubsampling;
sy = ySubsampling;
}
region.x = Math.multiplyExact(region.x, sx);
region.y = Math.multiplyExact(region.y, sy);
region.width = Math.multiplyExact(region.width, sx);
region.height = Math.multiplyExact(region.height, sy);
return region;
}
/**
* Invoked by {@link TileOrganizer} only. No other caller allowed.
* {@link #setSubsampling(Dimension)} must be invoked prior this method.
*
* <p>Note that invoking this method usually invalidate {@link #gridToCRS}.
* Calls to this method should be followed by {@link #translate(int, int)}
* for fixing the "gridToCRS" value.</p>
*
* @param region the region to assign to this tile in units of tile having finest resolution.
* @throws ArithmeticException if {@link #setSubsampling(Dimension)} method has not be invoked.
*/
final void setRegionOnFinestLevel(final Rectangle region) throws ArithmeticException {
assert Thread.holdsLock(this);
final int sx = xSubsampling;
final int sy = ySubsampling;
assert (region.width % sx) == 0 && (region.height % sy) == 0 : region;
x = region.x / sx;
y = region.y / sy;
width = region.width / sx;
height = region.height / sy;
}
/**
* Returns the subsampling relative to the tile having the finest resolution.
* The return value can be interpreted as "pixel size" relative to tiles having the finest resolution.
* This method never return {@code null}, and the width and height shall never be smaller than 1.
*
* @return the subsampling along <var>x</var> and <var>y</var> axes.
* @throws IllegalStateException if this tile has been {@linkplain #Tile(Rectangle, AffineTransform)
* created without location} and has not yet been processed by {@link TileOrganizer}.
*
* @see javax.imageio.ImageReadParam#setSourceSubsampling(int, int, int, int)
*/
public synchronized Dimension getSubsampling() throws IllegalStateException {
ensureDefined();
return new Dimension(xSubsampling, ySubsampling);
}
/**
* Sets the subsampling to the given dimension.
* Invoked by constructors and {@link TileOrganizer} only.
*/
final void setSubsampling(final Dimension subsampling) throws IllegalStateException {
// No assert Thread.holdsLock(this) because invoked from constructors.
if ((xSubsampling | ySubsampling) != 0) {
throw new IllegalStateException(); // Should never happen.
}
if (subsampling != null) {
ArgumentChecks.ensureBetween("width", 0, 0xFFFF, subsampling.width);
ArgumentChecks.ensureBetween("height", 0, 0xFFFF, subsampling.height);
xSubsampling = subsampling.width;
ySubsampling = subsampling.height;
} else {
xSubsampling = ySubsampling = 1;
}
}
/**
* If the user supplied transform is waiting for processing by {@link TileOrganizer}, returns it.
* Otherwise returns {@code null}. This method is for internal usage by {@link TileOrganizer} only.
*
* <p>This method clears the {@link #gridToCRS} field before to return. This is a way to tell that
* processing is in progress, and also a safety against transform usage while it may become invalid.</p>
*
* @return the transform, or {@code null} if none. This method does not clone the returned value -
* {@link TileOrganizer} will reference and modify directly that transform.
*/
final synchronized AffineTransform getPendingGridToCRS() {
if ((xSubsampling | ySubsampling) != 0) {
// No transform waiting to be processed.
return null;
}
final AffineTransform at = gridToCRS;
gridToCRS = null;
return at;
}
/**
* Returns the <q>grid to real world</q> transform, or {@code null} if unknown.
* This transform is derived from the value given to the constructor, but may not be identical
* since it may have been {@linkplain AffineTransform#translate(double, double) translated}
* in order to get a uniform grid geometry for every tiles.
*
* <h4>Tip</h4>
* The <a href="https://en.wikipedia.org/wiki/World_file">World File</a> coefficients of this tile
* (i.e. the <i>grid to CRS</i> transform that we would have if the pixel in the upper-left
* corner always had indices (0,0)) can be computed as below:
*
* {@snippet lang="java" :
* Point location = tile.getLocation();
* AffineTransform gridToCRS = new AffineTransform(tile.getGridToCRS());
* gridToCRS.translate(location.x, location.y);
* }
*
* @return the <q>grid to real world</q> transform mapping pixel
* {@linkplain PixelOrientation#UPPER_LEFT upper left} corner, or {@code null} if undefined.
* @throws IllegalStateException if this tile has been {@linkplain #Tile(Rectangle, AffineTransform)
* created without location} and has not yet been processed by {@link TileOrganizer}.
*/
public synchronized AffineTransform2D getGridToCRS() throws IllegalStateException {
ensureDefined();
/*
* The cast should not fail: if the `gridToCRS` is the one specified at construction time,
* then `ensureDefined()` should have thrown an IllegalStateException. Otherwise this tile
* have been processed by `TileOrganizer`, which has set an `AffineTransform2D` instance.
* If we get a ClassCastException below, then there is a bug in our pre/post conditions.
*/
return (AffineTransform2D) gridToCRS;
}
/**
* Sets the new <q>grid to real world</q> transform to use after the translation performed by
* {@link #translate(int, int)}, if any. The given instance should be immutable; it will not be cloned.
*
* @param at the <q>grid to real world</q> transform mapping pixel
* {@linkplain PixelOrientation#UPPER_LEFT upper left} corner.
* @throws IllegalStateException if another transform was already assigned to this tile.
*/
final void setGridToCRS(final AffineTransform at) throws IllegalStateException {
assert Thread.holdsLock(this);
if (gridToCRS == null) {
gridToCRS = at;
} else if (!gridToCRS.equals(at)) {
throw new IllegalStateException();
}
}
/**
* Translates this tile. For internal usage by {@link TileOrganizer} only.
*
* <p>Reminder: {@link #setGridToCRS(AffineTransform)} should be invoked after this method.</p>
*
* @param dx the translation to apply on <var>x</var> values (often 0).
* @param dy the translation to apply on <var>y</var> values (often 0).
*/
final void translate(final int dx, final int dy) {
assert Thread.holdsLock(this);
x = Math.addExact(x, dx);
y = Math.addExact(y, dy);
gridToCRS = null;
}
/**
* Returns a name for the tile format or tile input, or an empty value if none.
* The format name can be inferred for example from an {@link javax.imageio.spi.ImageReaderSpi}.
* The input name is typically (but not necessarily) a file name or URL.
*
* @param input {@code false} for the file format name, or {@code true} for the file input name.
* @return the format or input name.
*/
public Optional<String> getName(final boolean input) {
return Optional.empty();
}
/**
* Returns the image index to be given to the image reader for reading this tile.
* The default implementation returns 0.
*
* @return the image index, numbered from 0.
*
* @see ImageReader#read(int)
*/
public int getImageIndex() {
return 0;
}
/*
* Intentionally no implementation for `equals()` and `hashCode()`. Tile is an "almost immutable" class
* which can still be modified (only once) by MocaicCalculator, or by read operations during `getSize()`
* or `getRegion()` execution. This causes confusing behavior when used in an HashMap. We are better to
* rely on system identity. For example, `GridGroup` relies on the capability to locate Tiles in
* HashMap before and after they have been processed by `TileOrganizer`.
*/
/**
* Returns a string representation of this tile for debugging purposes.
*
* @return a string representation of this tile.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this)).append('[');
if ((xSubsampling | ySubsampling) != 0) {
buffer.append("location=(");
if (width == 0 && height == 0) {
final Point location = getLocation();
buffer.append(location.x).append(',').append(location.y);
} else try {
final Rectangle region = getRegion();
buffer.append(region.x).append(',').append(region.y)
.append("), size=(").append(region.width).append(',').append(region.height);
} catch (IOException e) {
/*
* Should not happen since we checked that `getRegion()` should be easy.
* If it happen anyway, put the exception message at the place where
* coordinates were supposed to appear, so we can debug.
*/
buffer.append(e);
}
final Dimension subsampling = getSubsampling();
buffer.append("), subsampling=(").append(subsampling.width)
.append(',').append(subsampling.height).append(')');
} else {
/*
* Location and subsampling not yet computed, so don't display it. We cannot
* invoke `getRegion()` neither since it would throw an IllegalStateException.
* Since we have to read the fields directly, make sure that this instance is
* not a subclass, otherwise those values may be wrong.
*/
if ((width != 0 || height != 0) && getClass() == Tile.class) {
buffer.append("size=(").append(width).append(',').append(height).append(')');
}
}
return buffer.append(']').toString();
}
/**
* Returns a string representation of a collection of tiles.
* The tiles are formatted in a table in iteration order.
*
* <p>This method is not public because it can consume a large amount of memory (the underlying
* {@link StringBuffer} can be quite large). Users are encouraged to use the method expecting a
* {@link Writer}, which may be expensive too but less than this method.</p>
*
* @param tiles the tiles to format in a table.
* @param maximum the maximum number of tiles to format. If there is more tiles, a message will be
* formatted below the table. A reasonable value like 5000 is recommended because
* attempt to format millions of tiles leads to {@link OutOfMemoryError}.
* @return a string representation of the given tiles as a table.
*/
static String toString(final Collection<Tile> tiles, final int maximum) {
final StringWriter writer = new StringWriter();
try {
writeTable(tiles, writer, maximum);
} catch (IOException e) {
// Should never happen since we are writing to a StringWriter.
throw new AssertionError(e);
}
return writer.toString();
}
/**
* Formats a collection of tiles in a table.
* The tiles are appended in iteration order.
*
* @param tiles the tiles to format in a table.
* @param out where to write the table.
* @param maximum the maximum number of tiles to format. If there is more tiles, a message will be
* formatted below the table. A reasonable value like 5000 is recommended because
* attempt to format millions of tiles leads to {@link OutOfMemoryError}.
* @throws IOException if an error occurred while writing to the given writer.
*/
public static void writeTable(final Collection<Tile> tiles, final Writer out, final int maximum) throws IOException {
int remaining = maximum;
final TableAppender table = new TableAppender(out);
table.setMultiLinesCells(false);
table.nextLine('═');
table.append("Format\tInput\tindex\tx\ty\twidth\theight\tdx\tdy\n");
table.nextLine('─');
table.setMultiLinesCells(true);
for (final Tile tile : tiles) {
if (--remaining < 0) {
break;
}
table.setCellAlignment(TableAppender.ALIGN_LEFT);
tile.getName(false).ifPresent(table::append); table.nextColumn();
tile.getName(true) .ifPresent(table::append); table.nextColumn();
table.setCellAlignment(TableAppender.ALIGN_RIGHT);
table.append(String.valueOf(tile.getImageIndex()));
table.nextColumn();
/*
* Extract now the tile information that we are going to format. Those information may
* be replaced by the information provided by getter methods (they should be the same,
* unless a subclass override those methods).
*/
int x = tile.x;
int y = tile.y;
int width = tile.width;
int height = tile.height;
int xSubsampling = tile.xSubsampling;
int ySubsampling = tile.ySubsampling;
try {
final Dimension subsampling = tile.getSubsampling();
xSubsampling = subsampling.width;
ySubsampling = subsampling.height;
try {
final Rectangle region = tile.getRegion();
x = region.x;
y = region.y;
width = region.width;
height = region.height;
} catch (IOException e) {
/*
* The (x,y) are likely to be correct since only (width,height) are read
* from the image file. So set only (width,height) to "unknown" and keep
* the remaining, with (x,y) obtained from direct access to Tile fields.
*/
width = 0;
height = 0;
}
} catch (IllegalStateException e) {
// Ignore. Format using the information read from the fields as a fallback.
}
table.append(String.valueOf(x));
table.nextColumn();
table.append(String.valueOf(y));
if ((width | height) != 0) {
table.nextColumn();
table.append(String.valueOf(width));
table.nextColumn();
table.append(String.valueOf(height));
} else {
table.nextColumn();
table.nextColumn();
}
if ((xSubsampling | ySubsampling) != 0) {
table.nextColumn();
table.append(String.valueOf(xSubsampling));
table.nextColumn();
table.append(String.valueOf(ySubsampling));
}
table.nextLine();
}
table.nextLine('═');
/*
* Table completed. Flushs to the writer and appends additional text if we have
* not formatted every tiles. IOException may be thrown starting from this point
* (the above code is not expected to throw any IOException).
*/
table.flush();
if (remaining < 0) {
out.write(Vocabulary.forLocale(null).getString(Vocabulary.Keys.More_1, tiles.size() - maximum));
out.write(System.lineSeparator());
}
}
}