use of org.apache.sis.coverage.grid.GridGeometry in project sis by apache.
the class RasterResource method read.
/**
* Loads a subset of the grid coverage represented by this resource.
*
* @param domain desired grid extent and resolution, or {@code null} for reading the whole domain.
* @param range 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all.
* @return the grid coverage for the specified domain and range.
* @throws DataStoreException if an error occurred while reading the grid coverage data.
*/
@Override
public GridCoverage read(final GridGeometry domain, final int... range) throws DataStoreException {
final long startTime = System.nanoTime();
final RangeArgument rangeIndices = validateRangeArgument(ranges.length, range);
final Variable first = data[bandDimension >= 0 ? 0 : rangeIndices.getFirstSpecified()];
final DataType dataType = first.getDataType();
if (bandDimension < 0) {
for (int i = 0; i < rangeIndices.getNumBands(); i++) {
final Variable variable = data[rangeIndices.getSourceIndex(i)];
if (!dataType.equals(variable.getDataType())) {
throw new DataStoreContentException(Resources.forLocale(getLocale()).getString(Resources.Keys.MismatchedVariableType_3, getFilename(), first.getName(), variable.getName()));
}
}
}
/*
* At this point the arguments and the state of this resource have been validated.
* There is three ways to read the data, determined by `bandDimension` value:
*
* • (bandDimension < 0): one variable per band (usual case).
* • (bandDimension = 0): one variable containing all bands, with bands in the first dimension.
* • (bandDimension > 0): one variable containing all bands, with bands in the last dimension.
*/
final GridGeometry targetDomain;
final DataBuffer imageBuffer;
final SampleDimension[] bands = new SampleDimension[rangeIndices.getNumBands()];
// By default, all bands start at index 0.
int[] bandOffsets = null;
try {
final GridDerivation targetGeometry = gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid((domain != null) ? domain : gridGeometry);
// Pixel indices of data to read.
GridExtent areaOfInterest = targetGeometry.getIntersection();
// Slice to read or subsampling to apply.
int[] subsampling = targetGeometry.getSubsampling();
// By default, one variable per band.
int numBuffers = bands.length;
// Adjust user-specified domain to data geometry.
targetDomain = targetGeometry.build();
if (bandDimension >= 0) {
areaOfInterest = rangeIndices.insertBandDimension(areaOfInterest, bandDimension);
subsampling = rangeIndices.insertSubsampling(subsampling, bandDimension);
if (bandDimension == 0) {
// Will be set to non-zero values later.
bandOffsets = new int[numBuffers];
}
// One variable for all bands.
numBuffers = 1;
}
/*
* Iterate over netCDF variables in the order they appear in the file, not in the order requested
* by the `range` argument. The intent is to perform sequential I/O as much as possible, without
* seeking backward. In the (uncommon) case where bands are one of the variable dimension instead
* than different variables, the reading of the whole variable occurs during the first iteration.
*/
Buffer[] sampleValues = new Buffer[numBuffers];
synchronized (lock) {
for (int i = 0; i < bands.length; i++) {
// In strictly increasing order.
int indexInResource = rangeIndices.getSourceIndex(i);
int indexInRaster = rangeIndices.getTargetIndex(i);
Variable variable = getVariable(indexInResource);
SampleDimension b = ranges[indexInResource];
if (b == null) {
ranges[indexInResource] = b = createSampleDimension(rangeIndices.builder(), variable, i);
}
bands[indexInRaster] = b;
if (bandOffsets != null) {
bandOffsets[indexInRaster] = i;
// Pixels interleaved in one bank: sampleValues.length = 1.
indexInRaster = 0;
}
if (i < numBuffers)
try {
// Optional.orElseThrow() below should never fail since Variable.read(…) wraps primitive array.
sampleValues[indexInRaster] = variable.read(areaOfInterest, subsampling).buffer().get();
} catch (ArithmeticException e) {
throw variable.canNotComputePosition(e);
}
}
}
/*
* The following block is executed only if all bands are in a single variable, and the bands dimension is
* the last one (in "natural" order). In such case, the sample model to construct is a BandedSampleModel.
* Contrarily to PixelInterleavedSampleModel (the case when the band dimension is first), banded sample
* model force us to split the buffer in a buffer for each band.
*/
if (bandDimension > 0) {
// Really > 0, not >= 0.
final int stride = Math.toIntExact(data[0].getBandStride());
Buffer values = sampleValues[0].limit(stride);
sampleValues = new Buffer[bands.length];
for (int i = 0; i < sampleValues.length; i++) {
if (i != 0) {
values = JDK9.duplicate(values);
final int p = values.limit();
values.position(p).limit(Math.addExact(p, stride));
}
sampleValues[i] = values;
}
}
/*
* Convert NIO Buffer into Java2D DataBuffer. May throw various RuntimeException.
*/
imageBuffer = RasterFactory.wrap(dataType.rasterDataType, sampleValues);
} catch (IOException | RuntimeException e) {
throw canNotRead(getFilename(), domain, e);
}
/*
* At this point the I/O operation is completed and sample values have been stored in a NIO buffer.
* Provide to `Raster` all information needed for building a `RenderedImage` when requested.
*/
if (imageBuffer == null) {
throw new DataStoreContentException(Errors.getResources(getLocale()).getString(Errors.Keys.UnsupportedType_1, dataType.name()));
}
final Variable main = data[visibleBand];
final Raster raster = new Raster(targetDomain, UnmodifiableArrayList.wrap(bands), imageBuffer, String.valueOf(identifier), rangeIndices.getPixelStride(), bandOffsets, visibleBand, main.decoder.convention().getColors(main));
logReadOperation(location, targetDomain, startTime);
return raster;
}
use of org.apache.sis.coverage.grid.GridGeometry in project sis by apache.
the class RasterResource method create.
/**
* Creates all grid coverage resources from the given decoder.
* This method shall be invoked in a method synchronized on {@link #lock}.
*
* @param decoder the implementation used for decoding the netCDF file.
* @param lock the lock to use in {@code synchronized(lock)} statements.
* @return all grid coverage resources.
* @throws IOException if an I/O operation was required and failed.
* @throws DataStoreException if a logical error occurred.
*/
public static List<Resource> create(final Decoder decoder, final Object lock) throws IOException, DataStoreException {
assert Thread.holdsLock(lock);
// Needs a clone because may be modified.
final Variable[] variables = decoder.getVariables().clone();
// Usually has only 1 element, sometime 2.
final List<Variable> siblings = new ArrayList<>(4);
// The raster resources to be returned.
final List<Resource> resources = new ArrayList<>(variables.length);
// For detecting name collisions.
final Map<GenericName, List<RasterResource>> byName = new HashMap<>();
for (int i = 0; i < variables.length; i++) {
final Variable variable = variables[i];
if (!VariableRole.isCoverage(variable)) {
// Skip variables that are not grid coverages.
continue;
}
final GridGeometry grid = variable.getGridGeometry();
if (grid == null) {
// Skip variables that are not grid coverages.
continue;
}
// Variable will the first band of raster.
siblings.add(variable);
String name = variable.getStandardName();
/*
* At this point we found a variable for which to create a resource. Most of the time, there is nothing else to do;
* the resource will have a single variable and the same name than that unique variable. The resulting raster will
* have only one band (sample dimension). However in some cases the raster should have more than one band:
*
* 1) if the variable has an extra dimension compared to the grid geometry;
* 2) of if two or more variables should be grouped together.
*
* The following if {…} else {…} blocks implement those two cases.
*/
final List<Dimension> gridDimensions = variable.getGridDimensions();
final int dataDimension = gridDimensions.size();
final int gridDimension = grid.getDimension();
final int bandDimension, numBands;
if (dataDimension != gridDimension) {
// One variable dimension is interpreted as bands.
bandDimension = variable.bandDimension;
// Note: "natural" → netCDF index conversion.
Dimension dim = gridDimensions.get(dataDimension - 1 - bandDimension);
numBands = Math.toIntExact(dim.length());
if (dataDimension != gridDimension + 1 || (bandDimension > 0 && bandDimension != gridDimension)) {
/*
* One of the following restrictions it not met for the requested data:
*
* - Only 1 dimension can be used for bands. Variables with 2 or more band dimensions are not supported.
* - The dimension for bands shall be either the first or the last dimension; it can not be in the middle.
*/
throw new DataStoreContentException(Resources.forLocale(decoder.listeners.getLocale()).getString(Resources.Keys.UnmappedDimensions_4, name, decoder.getFilename(), dataDimension, gridDimension));
}
} else {
/*
* At this point we found a variable where all dimensions are in the CRS. This is the usual case;
* there is no band explicitly declared in the netCDF file. However in some cases, we should put
* other variables together with the one we just found. Example:
*
* 1) baroclinic_eastward_sea_water_velocity
* 2) baroclinic_northward_sea_water_velocity
*
* We use the "eastward" and "northward" keywords for recognizing such pairs, providing that everything else in the
* name is the same and the grid geometries are the same.
*/
// No dimension to be interpreted as bands.
bandDimension = -1;
final DataType type = variable.getDataType();
for (final String keyword : VECTOR_COMPONENT_NAMES) {
final int prefixLength = name.indexOf(keyword);
if (prefixLength >= 0) {
int suffixStart = prefixLength + keyword.length();
int suffixLength = name.length() - suffixStart;
for (int j = i; ++j < variables.length; ) {
final Variable candidate = variables[j];
if (!VariableRole.isCoverage(candidate)) {
// For avoiding to revisit that variable again.
variables[j] = null;
continue;
}
final String cn = candidate.getStandardName();
if (cn.regionMatches(cn.length() - suffixLength, name, suffixStart, suffixLength) && cn.regionMatches(0, name, 0, prefixLength) && candidate.getDataType() == type && grid.equals(candidate.getGridGeometry())) {
/*
* Found another variable with the same name except for the keyword. Verify that the
* keyword is replaced by another word in the vector component keyword list. If this
* is the case, then we consider that those two variables should be kept together.
*/
for (final String k : VECTOR_COMPONENT_NAMES) {
if (cn.startsWith(k, prefixLength)) {
siblings.add(candidate);
variables[j] = null;
break;
}
}
}
}
/*
* If we have more than one variable, omit the keyword from the name. For example instead
* of "baroclinic_eastward_sea_water_velocity", construct "baroclinic_sea_water_velocity".
* Note that we may need to remove duplicated '_' character after keyword removal.
*/
if (siblings.size() > 1) {
if (suffixLength != 0) {
final int c = name.codePointAt(suffixStart);
if ((prefixLength != 0) ? (c == name.codePointBefore(prefixLength)) : (c == '_')) {
suffixStart += Character.charCount(c);
}
}
name = new StringBuilder(name).delete(prefixLength, suffixStart).toString();
}
}
}
numBands = siblings.size();
}
final RasterResource r = new RasterResource(decoder, name.trim(), grid, siblings, numBands, bandDimension, lock);
r.addToNameMap(byName);
resources.add(r);
siblings.clear();
}
/*
* At this point all resources have been prepared. If the same standard name is used by more than one resource,
* replace the standard name by the identifier for all resources in collision (an "all or nothing" replacement).
* The identifiers should be unique, which should resolve the name collision. We nevertheless check again after
* renaming until there is nothing to change.
*/
boolean changed;
do {
changed = false;
List<RasterResource> collisions = null;
for (final Iterator<List<RasterResource>> it = byName.values().iterator(); it.hasNext(); ) {
final List<RasterResource> rs = it.next();
if (rs.size() >= 2) {
// Remove resources before to re-insert them in next loop below.
it.remove();
if (collisions == null) {
collisions = rs;
} else {
collisions.addAll(rs);
}
}
}
if (collisions != null) {
// After above loop because may change the byName` map.
for (final RasterResource r : collisions) {
changed |= r.resolveNameCollision(decoder);
// For checking if new names cause new collisions.
r.addToNameMap(byName);
}
}
} while (changed);
return resources;
}
use of org.apache.sis.coverage.grid.GridGeometry in project sis by apache.
the class Variable method getGridGeometry.
/**
* Returns the grid geometry for this variable, or {@code null} if this variable is not a data cube.
* Not all variables have a grid geometry. For example collections of features do not have such grid.
* The same grid geometry may be shared by many variables.
* The grid may have fewer {@linkplain Grid#getDimensions() dimensions} than this variable,
* in which case the additional {@linkplain #getGridDimensions() variable dimensions} can be considered as bands.
*
* @return the grid geometry for this variable, or {@code null} if none.
* @throws IOException if an error occurred while reading the data.
* @throws DataStoreException if a logical error occurred.
*/
public final GridGeometry getGridGeometry() throws IOException, DataStoreException {
if (!gridDetermined) {
// Set first so we don't try twice in case of failure.
gridDetermined = true;
final GridMapping gridMapping = GridMapping.forVariable(this);
final GridAdjustment adjustment = new GridAdjustment();
final Grid info = getGrid(adjustment);
if (info != null) {
/*
* This variable may have more dimensions than the grid. We need to reduce the list to the same
* dimensions than the ones in the grid. We can not take Grid.getDimensions() directly because
* those dimensions may not have the same length (this mismatch is handled in the next block).
*/
// In netCDF order.
List<Dimension> dimensions = getGridDimensions();
final int dataDimension = dimensions.size();
if (dataDimension > info.getSourceDimensions()) {
boolean copied = false;
// Also in netCDF order.
final List<Dimension> toKeep = info.getDimensions();
final int numToKeep = toKeep.size();
for (int i = 0; i < numToKeep; i++) {
Dimension expected = toKeep.get(i);
expected = adjustment.gridToVariable.getOrDefault(expected, expected);
/*
* At this point, `expected` is a dimension of the variable that we expect to find at
* current index `i`. If we do not find that dimension, then the unexpected dimension
* is assumed to be a band. We usually remove at most one element. If removal results
* in a list too short, it would be a bug in the way we computed `toKeep`.
*/
while (!expected.equals(dimensions.get(i))) {
if (!copied) {
copied = true;
dimensions = new ArrayList<>(dimensions);
}
/*
* It is possible that we never reach this point if the unexpected dimension is last.
* However in such case the dimension to declare is the last one in netCDF order,
* which corresponds to the first dimension (i.e. dimension 0) in "natural" order.
* Since the `bandDimension` field is initialized to zero, its value is correct.
*/
// Convert netCDF order to "natural" order.
bandDimension = dataDimension - 1 - i;
dimensions.remove(i);
for (int j = dimensions.size(); --j >= i; ) {
dimensions.set(j, dimensions.get(j).decrementIndex());
}
if (dimensions.size() < numToKeep) {
// Should not happen (see above comment).
throw new InternalDataStoreException();
}
}
}
/*
* At this point `dimensions` may still be longer than `toKeep` but it does not matter.
* We only need that for any index i < numToKeep, dimensions.get(i) corresponds to the
* dimension at the same index in the grid.
*/
}
/*
* Compare the size of the variable with the size of the localization grid.
* If they do not match, then there is a scale factor between the two that
* needs to be applied.
*/
GridGeometry grid = info.getGridGeometry(decoder);
if (grid != null) {
if (grid.isDefined(GridGeometry.EXTENT)) {
GridExtent extent = grid.getExtent();
final long[] sizes = new long[extent.getDimension()];
boolean needsResize = false;
for (int i = sizes.length; --i >= 0; ) {
// Convert "natural order" index into netCDF index.
final int d = (sizes.length - 1) - i;
sizes[i] = dimensions.get(d).length();
if (!needsResize) {
needsResize = (sizes[i] != extent.getSize(i));
}
}
if (needsResize) {
final double[] dataToGridIndices = adjustment.dataToGridIndices();
if (dataToGridIndices == null || dataToGridIndices.length < sizes.length) {
warning(Variable.class, "getGridGeometry", Resources.Keys.ResamplingIntervalNotFound_2, getFilename(), getName());
return null;
}
extent = extent.resize(sizes);
grid = GridAdjustment.scale(grid, extent, info.getAnchor(), dataToGridIndices);
}
}
/*
* At this point we finished to build a grid geometry from the information provided by axes.
* If there is grid mapping attributes (e.g. "EPSG_code", "ESRI_pe_string", "GeoTransform",
* "spatial_ref", etc.), substitute some parts of the grid geometry by the parts built from
* those attributes.
*/
if (gridMapping != null) {
grid = gridMapping.adaptGridCRS(this, grid, info.getAnchor());
}
}
gridGeometry = grid;
} else if (gridMapping != null) {
gridGeometry = gridMapping.createGridCRS(this);
}
}
return gridGeometry;
}
use of org.apache.sis.coverage.grid.GridGeometry in project sis by apache.
the class CoverageSubset method clip.
/**
* Clips the given domain to the area of interest specified by the query. If any grid geometry is null,
* the other one is returned. The {@code domain} argument should be the domain to read as specified to
* {@link #read(GridGeometry, int...)}, or the full {@code CoverageSubset} domain if no value were given
* to the {@code read(…)} method.
*
* @param domain the domain requested in a read operation, or {@code null}.
* @param rounding whether to clip to nearest box or an enclosing box.
* @param clipping whether to clip the resulting extent to the specified {@code domain} extent.
* @return intersection of the given grid geometry with the query domain.
* @throws DataStoreException if the intersection can not be computed.
*/
private GridGeometry clip(final GridGeometry domain, final GridRoundingMode rounding, final GridClippingMode clipping) throws DataStoreException {
final GridGeometry areaOfInterest = query.getSelection();
if (domain == null)
return areaOfInterest;
if (areaOfInterest == null)
return domain;
try {
final GridDerivation derivation = domain.derive().rounding(rounding).clipping(clipping);
final int expansion = query.getSourceDomainExpansion();
if (expansion != 0) {
final int[] margins = new int[domain.getDimension()];
Arrays.fill(margins, expansion);
derivation.margin(margins);
}
return derivation.subgrid(areaOfInterest).build();
} catch (IllegalArgumentException | IllegalStateException e) {
final String msg = Resources.forLocale(getLocale()).getString(Resources.Keys.CanNotIntersectDataWithQuery_1, getSourceName());
final Throwable cause = e.getCause();
if (cause instanceof FactoryException || cause instanceof TransformException) {
throw new DataStoreReferencingException(msg, cause);
} else if (e instanceof DisjointExtentException) {
throw new NoSuchDataException(msg, e);
} else {
throw new DataStoreException(msg, e);
}
}
}
use of org.apache.sis.coverage.grid.GridGeometry in project sis by apache.
the class MapCanvas method repaint.
/**
* Invoked when the map content needs to be rendered again.
* It may be because the map has new content, or because the viewed region moved or has been zoomed.
* This method starts the rendering process immediately, unless a rendering is already in progress.
*
* @see #requestRepaint()
*/
final void repaint() {
assert Platform.isFxApplicationThread();
/*
* If a rendering is already in progress, do not send a new request now.
* Wait for current rendering to finish; a new one will be automatically
* requested if content changes are detected after the rendering.
*/
if (renderingInProgress != null) {
if (renderingInProgress instanceof Delayed) {
renderingInProgress.cancel(true);
renderingInProgress = null;
} else {
contentChangeCount++;
return;
}
}
hasError = false;
// Avoid that `requestRepaint(…)` trig new paints.
isRendering.set(true);
renderingStartTime = System.nanoTime();
try {
/*
* If a new canvas size is known, inform the parent `PlanarCanvas` about that.
* It may cause a recomputation of the "objective to display" transform.
*/
if (sizeChanged) {
sizeChanged = false;
final Pane view = floatingPane;
Envelope2D bounds = new Envelope2D(null, view.getLayoutX(), view.getLayoutY(), view.getWidth(), view.getHeight());
if (bounds.isEmpty())
return;
setDisplayBounds(bounds);
}
/*
* Compute the `objectiveToDisplay` only before the first rendering, because the display
* bounds may not be known before (it may be zero at the time `MapCanvas` is initialized).
* This code is executed only once for a new map.
*/
if (invalidObjectiveToDisplay) {
final Envelope2D target = getDisplayBounds();
if (target == null) {
// Bounds are still unknown. Another repaint event will happen when they will become known.
return;
}
invalidObjectiveToDisplay = false;
final GridExtent extent = new GridExtent(null, new long[] { Math.round(target.getMinX()), Math.round(target.getMinY()) }, new long[] { Math.round(target.getMaxX()), Math.round(target.getMaxY()) }, false);
/*
* If `setObjectiveBounds(…)` has been invoked (as it should be), initialize the affine
* transform to values which will allow this canvas to contain fully the objective bounds.
* Otherwise the transform is initialized to an identity transform (should not happen often).
* If a CRS is present, it is used for deciding if we need to swap or flip axes.
*/
CoordinateReferenceSystem objectiveCRS;
final LinearTransform crsToDisplay;
if (objectiveBounds != null) {
objectiveCRS = objectiveBounds.getCoordinateReferenceSystem();
final MatrixSIS m;
if (objectiveCRS != null) {
AxisDirection[] srcAxes = CoordinateSystems.getAxisDirections(objectiveCRS.getCoordinateSystem());
m = Matrices.createTransform(objectiveBounds, srcAxes, target, toDisplayDirections(srcAxes));
} else {
m = Matrices.createTransform(objectiveBounds, target);
}
Matrices.forceUniformScale(m, 0, new double[] { target.getCenterX(), target.getCenterY() });
crsToDisplay = MathTransforms.linear(m);
if (objectiveCRS == null) {
objectiveCRS = extent.toEnvelope(crsToDisplay.inverse()).getCoordinateReferenceSystem();
/*
* Above code tried to provide a non-null CRS on a "best effort" basis. The objective CRS
* may still be null, there is no obvious answer against that. It is not the display CRS
* if the "display to objective" transform is not identity. A grid CRS is not appropriate
* neither, otherwise `extent.toEnvelope(…)` would have found it.
*/
}
} else {
objectiveCRS = getDisplayCRS();
crsToDisplay = MathTransforms.identity(BIDIMENSIONAL);
}
setGridGeometry(new GridGeometry(extent, PixelInCell.CELL_CORNER, crsToDisplay.inverse(), objectiveCRS));
transform.setToIdentity();
}
} catch (TransformException | RenderException ex) {
restoreCursorAfterPaint();
isRendering.set(false);
errorOccurred(ex);
return;
}
/*
* If a temporary zoom, rotation or translation has been applied using JavaFX transform API,
* replace that temporary transform by a "permanent" adjustment of the `objectiveToDisplay`
* transform. It allows SIS to get new data for the new visible area and resolution.
* Do not reset `transform` to identity now; we need to continue accumulating gestures
* that may happen while the rendering is done in a background thread.
*/
changeInProgress.setToTransform(transform);
if (!transform.isIdentity()) {
transformDisplayCoordinates(new AffineTransform(transform.getMxx(), transform.getMyx(), transform.getMxy(), transform.getMyy(), transform.getTx(), transform.getTy()));
}
/*
* Invoke `createWorker(…)` only after we finished above configuration, because that method
* may take a snapshot of current canvas state in preparation for use in background threads.
* Take the value of `contentChangeCount` only now because above code may have indirect calls
* to `requestRepaint()`.
*/
renderedContentStamp = contentChangeCount;
final Renderer context = createRenderer();
if (context != null && context.initialize(floatingPane)) {
final Task<?> worker = createWorker(context);
assert renderingInProgress == null;
BackgroundThreads.execute(worker);
// Set after we know that the task has been scheduled.
renderingInProgress = worker;
if (!isMouseChangeScheduled) {
DelayedExecutor.schedule(new CursorChange());
isMouseChangeScheduled = true;
}
} else {
if (!hasError) {
clearError();
}
isRendering.set(false);
restoreCursorAfterPaint();
}
}
Aggregations