This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 005c183 Allow `GridEvaluator` to handle wraparound axes. 005c183 is described below commit 005c183b5fb071c314c0180793e8712f491aeaae Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Feb 21 11:23:55 2022 +0100 Allow `GridEvaluator` to handle wraparound axes. --- .../coverage/grid/FractionalGridCoordinates.java | 2 +- .../apache/sis/coverage/grid/GridEvaluator.java | 215 +++++++++++++++++++-- .../sis/coverage/grid/GridCoverage2DTest.java | 37 +++- .../apache/sis/geometry/WraparoundAdjustment.java | 26 +-- .../internal/referencing/WraparoundApplicator.java | 2 +- .../internal/referencing/WraparoundAxesFinder.java | 86 +++++++++ 6 files changed, 333 insertions(+), 35 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java index accd273..f243986 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java @@ -448,7 +448,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable long validMin = 0; long validMax = 0; double distance = 0; - for (int i=bounds.getDimension(); --i >= 0;) { + for (int i = bounds.getDimension(); --i >= 0;) { final long low = bounds.getLow(i); final long high = bounds.getHigh(i); final double c = coordinates[i]; diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java index b8ddb4b..f246d44 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridEvaluator.java @@ -16,10 +16,12 @@ */ package org.apache.sis.coverage.grid; +import java.util.Arrays; import java.awt.image.RenderedImage; import org.opengis.util.FactoryException; import org.opengis.geometry.DirectPosition; import org.opengis.referencing.datum.PixelInCell; +import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.CoordinateOperation; @@ -28,9 +30,13 @@ import org.opengis.coverage.CannotEvaluateException; import org.opengis.coverage.PointOutsideCoverageException; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.internal.coverage.j2d.ImageUtilities; -import org.apache.sis.referencing.CRS; +import org.apache.sis.internal.referencing.DirectPositionView; +import org.apache.sis.internal.referencing.WraparoundAxesFinder; import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.referencing.CRS; +import org.apache.sis.internal.system.Modules; import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.logging.Logging; /** @@ -70,10 +76,10 @@ public class GridEvaluator implements GridCoverage.Evaluator { * This is cached for avoiding the costly process of fetching a coordinate operation * in the common case where the coordinate reference systems did not changed. */ - private MathTransform crsToGrid; + private MathTransform sourceToGrid; /** - * Grid coordinates after {@link #crsToGrid} conversion. + * Grid coordinates after {@link #sourceToGrid} conversion. * * @see #toGridPosition(DirectPosition) */ @@ -93,6 +99,35 @@ public class GridEvaluator implements GridCoverage.Evaluator { */ private boolean nullIfOutside; + + // ―――――――― Following fields are for the handling of wraparound axes. ――――――――――――――――――――― + + /** + * A bitmask of grid dimensions that need to be verified for wraparound axes. + */ + private long wraparoundAxes; + + /** + * Coverage extent converted to floating point numbers, only for the grid dimensions having + * a bit set to 1 in {@link #wraparoundAxes} bitmask. The length of this array is the number + * of bits set in {@link #wraparoundAxes} multiplied by 2. Elements are (lower, upper) tuples. + */ + private double[] wraparoundExtent; + + /** + * Transform from grid coordinates to the CRS where wraparound axes may exist. + * It is sometime the same transform than {@code gridToCRS} but not always. + * It may differ for example if a projected CRS has been replaced by a geographic CRS. + */ + private MathTransform gridToWraparound; + + /** + * The span (maximum - minimum) of wraparound axes, with 0 value for axes that are not wraparound. + * The length of this array may be shorter than the CRS number of dimensions if all remaining axes + * are not wraparound axes. + */ + private double[] periods; + /** * Creates a new evaluator for the given coverage. This constructor is protected for allowing * {@link GridCoverage} subclasses to provide their own {@code GridEvaluator} implementations. @@ -118,6 +153,80 @@ public class GridEvaluator implements GridCoverage.Evaluator { } /** + * Returns {@code true} if this evaluator is allowed to wraparound coordinates that are outside the grid. + * The initial value is {@code false}. This method may continue to return {@code false} even after a call + * to {@code setWraparoundEnabled(true)} if no wraparound axis has been found in the coverage CRS. + * + * @return {@code true} if this evaluator may wraparound coordinates that are outside the grid. + * + * @since 1.2 + */ + public boolean isWraparoundEnabled() { + return (wraparoundAxes != 0); + } + + /** + * Specifies whether this evaluator is allowed to wraparound coordinates that are outside the grid. + * If {@code true} and if a given coordinate is outside the grid, then this evaluator may translate + * the point along a wraparound axis in an attempt to get the point inside the grid. For example if + * the coverage CRS has a longitude axis, then the evaluator may translate the longitude value by a + * multiple of 360°. + * + * @param allow whether to allow wraparound of coordinates that are outside the grid. + * + * @since 1.2 + */ + public void setWraparoundEnabled(final boolean allow) { + wraparoundAxes = 0; + if (allow) try { + final WraparoundAxesFinder f = new WraparoundAxesFinder(coverage.getCoordinateReferenceSystem()); + if ((periods = f.periods()) != null) { + final GridGeometry gridGeometry = coverage.getGridGeometry(); + final GridExtent extent = gridGeometry.getExtent(); + MathTransform gridToCRS = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER); + gridToWraparound = MathTransforms.concatenate(gridToCRS, f.preferredToSpecified.inverse()); + final Matrix m = gridToWraparound.derivative(new DirectPositionView.Double(extent.getPointOfInterest())); + /* + * `gridToWraparound` is the transform from grid coordinates to a CRS where wraparound axes exist. + * It may be the coverage CRS or its base CRS. The wraparound axes are identified by `periods`. + * The Jacobian matrix tells us which grid axes are dependencies of the wraparound axes. + * + * Note: the use of this matrix is not fully reliable with non-linear `gridToCRS` transforms + * because it is theoretically possible that a coefficient is zero at the point of interest + * by pure coincidence but would be non-zero at another location, + * But we think that it would require a transform with unlikely properties + * (e.g. transforming parallels to vertical straight lines at some places). + */ + for (int j = periods.length; --j >= 0;) { + if (periods[j] > 0) { // Find target dimensions (CRS) having wraparound axis. + for (int i = Math.min(m.getNumCol(), Long.SIZE); --i >= 0;) { + if (m.getElement(j, i) != 0) { + wraparoundAxes |= (1L << i); // Mark sources (grid dimensions) dependent of target (CRS dimensions). + } + } + } + } + /* + * Get the grid extent only for the grid axes that are connected to wraparound CRS axes. + * There is at least one such axis, otherwise `periods` would have been null. + */ + wraparoundExtent = new double[Long.bitCount(wraparoundAxes) << 1]; + long axes = wraparoundAxes; + int j = 0; + do { + final int i = Long.numberOfTrailingZeros(axes); + wraparoundExtent[j++] = extent.getLow(i); + wraparoundExtent[j++] = extent.getHigh(i); + axes &= ~(1L << i); + } while (axes != 0); + assert wraparoundExtent.length == j : j; + } + } catch (TransformException e) { + recoverableException("setWraparoundEnabled", e); + } + } + + /** * Returns whether to return {@code null} instead of throwing an exception if a point is outside coverage bounds. * The default value is {@code false}, which means that the default {@link #apply(DirectPosition)} behavior is to * throw {@link PointOutsideCoverageException} for points outside bounds. @@ -172,7 +281,7 @@ public class GridEvaluator implements GridCoverage.Evaluator { public double[] apply(final DirectPosition point) throws CannotEvaluateException { /* * TODO: instead of restricting to a single point, keep the automatic size (1 or 2), - * invoke render for each plan, then interpolate. We would keep a value of 1 in the + * invoke render for each slice, then interpolate. We would keep a value of 1 in the * size array if we want to disable interpolation in some particular axis (e.g. time). */ final GridGeometry gridGeometry = coverage.gridGeometry; @@ -253,7 +362,7 @@ public class GridEvaluator implements GridCoverage.Evaluator { */ final FractionalGridCoordinates.Position toGridPosition(final DirectPosition point) throws TransformException { final CoordinateReferenceSystem crs = point.getCoordinateReferenceSystem(); - if (crs != sourceCRS || crsToGrid == null) { + if (crs != sourceCRS || sourceToGrid == null) { final GridGeometry gridGeometry = coverage.getGridGeometry(); MathTransform tr = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER).inverse(); if (crs != null) try { @@ -264,15 +373,99 @@ public class GridEvaluator implements GridCoverage.Evaluator { } catch (FactoryException e) { throw new TransformException(e.getMessage(), e); } - position = new FractionalGridCoordinates.Position(tr.getTargetDimensions()); - crsToGrid = tr; - sourceCRS = crs; + position = new FractionalGridCoordinates.Position(tr.getTargetDimensions()); + sourceCRS = crs; + sourceToGrid = tr; } - final DirectPosition result = crsToGrid.transform(point, position); + /* + * Transform geospatial coordinates to grid coordinates. Result is unconditionally stored + * in the `position` object, which will be copied by the caller if needed for public API. + */ + final DirectPosition result = sourceToGrid.transform(point, position); if (result != position) { - final double[] coordinates = result.getCoordinate(); - System.arraycopy(coordinates, 0, position.coordinates, 0, position.coordinates.length); + // Should not happen, but be paranoiac. + final double[] coordinates = position.coordinates; + System.arraycopy(result.getCoordinate(), 0, coordinates, 0, coordinates.length); + } + /* + * If most cases, the work of this method ends here. The remaining code in this method + * is for handling wraparound axes. If a coordinate is outside the coverage extent, + * check if a wraparound on some axes would bring the coordinates inside the extent. + * The first step is to get the point closest to the extent. + */ + long axes = wraparoundAxes; + if (axes != 0) { + double[] coordinates = position.coordinates; + long outsideAxes = 0; + int j = 0; + do { + final int i = Long.numberOfTrailingZeros(axes); + final double c = coordinates[i]; + double border; + if (c < (border = wraparoundExtent[j++]) || c > (border = wraparoundExtent[j])) { + if (outsideAxes == 0) { + final int n = coordinates.length; + coordinates = Arrays.copyOf(coordinates, 2*Math.max(n, gridToWraparound.getTargetDimensions())); + System.arraycopy(coordinates, 0, coordinates, n, n); + } + coordinates[i] = border; + outsideAxes |= (1L << i); + } + j++; // Outside above `if (…)` statement because increment needs to be unconditional. + axes &= ~(1L << i); + } while (axes != 0); + assert wraparoundExtent.length == j : j; + /* + * If a coordinate was found outside the grid, transform to a CRS where we can apply shift. + * It may be the same CRS than the coverage CRS or the source CRS, but not necessarily. + * Current version does not try to optimize by checking if `point` argument can be reused. + */ + if (outsideAxes != 0) { + gridToWraparound.transform(coordinates, 0, coordinates, 0, 2); + final int s = gridToWraparound.getTargetDimensions(); + for (int i = periods.length; --i >= 0;) { + final double period = periods[i]; + if (period > 0) { + /* + * Compute the shift that was necessary for moving the point inside the grid, + * then round that shift to an integer amount of periods. Modify the original + * coordinate by applying that modified translation. + */ + final int oi = i + s; + double shift = coordinates[i] - coordinates[oi]; + shift = Math.copySign(Math.ceil(Math.abs(shift) / period), shift) * period; + coordinates[oi] += shift; + } + } + /* + * Convert back the shifted point to grid coordinates, then check again if the new point + * is inside the grid extent. If this is not the case, we will return the old position + * on the assumption that it will be less confusing to the user. + */ + gridToWraparound.inverse().transform(coordinates, s, coordinates, 0, 1); + j = 0; + do { + final int i = Long.numberOfTrailingZeros(outsideAxes); + final double c = coordinates[i]; + if (c < wraparoundExtent[j++] || c > wraparoundExtent[j++]) { + return position; + } + outsideAxes &= ~(1L << i); + } while (outsideAxes != 0); + System.arraycopy(coordinates, 0, position.coordinates, 0, position.coordinates.length); + } } return position; } + + /** + * Invoked when a recoverable exception occurred. + * Those exceptions must be minor enough that they can be silently ignored in most cases. + * + * @param caller the method where exception occurred. + * @param exception the exception that occurred. + */ + private static void recoverableException(final String caller, final TransformException exception) { + Logging.recoverableException(Logging.getLogger(Modules.RASTER), GridEvaluator.class, caller, exception); + } } diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java index 6a8be0a..bf19bde 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridCoverage2DTest.java @@ -29,6 +29,7 @@ import org.opengis.geometry.DirectPosition; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.coverage.PointOutsideCoverageException; import org.opengis.referencing.operation.MathTransform1D; +import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.datum.PixelInCell; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.geometry.DirectPosition2D; @@ -36,7 +37,9 @@ import org.apache.sis.internal.coverage.j2d.RasterFactory; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.Units; import org.apache.sis.referencing.crs.HardCodedCRS; +import org.apache.sis.referencing.operation.matrix.Matrix3; import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.test.DependsOnMethod; import org.apache.sis.test.TestCase; import org.junit.Test; @@ -71,8 +74,16 @@ public strictfp class GridCoverage2DTest extends TestCase { * } */ private GridCoverage createTestCoverage() { + return createTestCoverage(MathTransforms.identity(2)); + } + + /** + * Sames as {@link #createTestCoverage()} except that the "grid to CRS" transform can be specified. + * The domain of source grid indices is the [0 … 1] range in all dimensions. + */ + private GridCoverage createTestCoverage(final MathTransform gridToCRS) { final GridGeometry grid = new GridGeometry(new GridExtent(GRID_SIZE, GRID_SIZE), - PixelInCell.CELL_CENTER, MathTransforms.identity(2), HardCodedCRS.WGS84); + PixelInCell.CELL_CENTER, gridToCRS, HardCodedCRS.WGS84); final MathTransform1D toUnits = (MathTransform1D) MathTransforms.linear(0.5, 100); final SampleDimension sd = new SampleDimension.Builder().setName("Some kind of height") @@ -216,6 +227,30 @@ public strictfp class GridCoverage2DTest extends TestCase { } /** + * Tests {@link GridEvaluator#apply(DirectPosition)} with a wraparound on the longitude axis. + * This method tests a coordinate that would be outside the grid if wraparound was not applied. + * + * @todo Not yet implemented. One potential place where to implement this functionality could be + * {@link GridEvaluator#toGridPosition(DirectPosition)}. + */ + @Test + @DependsOnMethod("testEvaluator") + public void testEvaluatorWithWraparound() { + final Matrix3 gridToCRS = new Matrix3(); + gridToCRS.m00 = 100; // Scale + gridToCRS.m02 = 100; // Offset + final GridEvaluator evaluator = createTestCoverage(MathTransforms.linear(gridToCRS)).evaluator(); + evaluator.setWraparoundEnabled(true); + assertArrayEquals(new double[] {2}, evaluator.apply(new DirectPosition2D(100, 0)), STRICT); + assertArrayEquals(new double[] {5}, evaluator.apply(new DirectPosition2D(200, 0)), STRICT); + /* + * Following tests fail if wraparound is not applied by `GridEvaluator`. + */ + assertArrayEquals(new double[] {5}, evaluator.apply(new DirectPosition2D(200 - 360, 0)), STRICT); + assertArrayEquals(new double[] {2}, evaluator.apply(new DirectPosition2D(100 - 360, 0)), STRICT); + } + + /** * Verifies that calling {@link GridCoverage#render(GridExtent)} with a sub-extent (crop operation) * returns precisely the requested area, not a smaller or bigger one. */ diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/WraparoundAdjustment.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/WraparoundAdjustment.java index 94aed8e..505b5f5 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/WraparoundAdjustment.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/WraparoundAdjustment.java @@ -19,9 +19,7 @@ package org.apache.sis.geometry; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.opengis.util.FactoryException; -import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.crs.CoordinateReferenceSystem; -import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.CoordinateOperation; @@ -31,7 +29,7 @@ import org.apache.sis.referencing.CRS; import org.apache.sis.math.MathFunctions; import org.apache.sis.internal.metadata.ReferencingServices; import org.apache.sis.internal.referencing.ReferencingUtilities; -import org.apache.sis.internal.referencing.WraparoundApplicator; +import org.apache.sis.internal.referencing.WraparoundAxesFinder; import org.apache.sis.internal.system.Loggers; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.ArgumentChecks; @@ -262,24 +260,10 @@ public class WraparoundAdjustment { * Replace the input CRS by an intermediate CRS where wraparound axes can be found. * We try to select a CRS as close as possible (simplest transform) to the input. */ - if (crs instanceof ProjectedCRS) { - final ProjectedCRS p = (ProjectedCRS) crs; - crs = p.getBaseCRS(); // Geographic, so a wraparound axis certainly exists. - inputToShiftable = p.getConversionFromBase().getMathTransform().inverse(); - } else { - // TODO: we should handle the case of CompoundCRS before to fallback on identity. - inputToShiftable = MathTransforms.identity(ReferencingUtilities.getDimension(crs)); - } - final CoordinateSystem cs = crs.getCoordinateSystem(); - for (int i = cs.getDimension(); --i >= 0;) { - final double period = WraparoundApplicator.range(cs, i); - if (period > 0) { - if (periods == null) { - transformDomain(crs); - periods = new double[i + 1]; - } - periods[i] = period; - } + final WraparoundAxesFinder f = new WraparoundAxesFinder(crs); + inputToShiftable = f.preferredToSpecified.inverse(); + if ((periods = f.periods()) != null) { + transformDomain(f.preferredCRS); } } } diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundApplicator.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundApplicator.java index 411f3bb..9f6a5c6 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundApplicator.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundApplicator.java @@ -178,7 +178,7 @@ public final class WraparoundApplicator { * @param dimension dimension of the axis to test. * @return the wraparound range, or {@link Double#NaN} if none. */ - public static double range(final CoordinateSystem cs, final int dimension) { + static double range(final CoordinateSystem cs, final int dimension) { final CoordinateSystemAxis axis = cs.getAxis(dimension); if (axis != null && RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) { double period = axis.getMaximumValue() - axis.getMinimumValue(); diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAxesFinder.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAxesFinder.java new file mode 100644 index 0000000..b381f50 --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/WraparoundAxesFinder.java @@ -0,0 +1,86 @@ +/* + * 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.internal.referencing; + +import org.opengis.referencing.cs.CoordinateSystem; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.crs.ProjectedCRS; +import org.opengis.referencing.operation.MathTransform; +import org.apache.sis.referencing.operation.transform.MathTransforms; + + +/** + * Finds the axes where wraparound may happen in a CRS. The search may be indirect. + * For example if the given CRS is projected, this class will search in geographic CRS. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final class WraparoundAxesFinder { + /** + * The CRS that may contain wraparound axes. Geographic CRS are preferred, + * but will be the CRS specified at construction time if we found nothing better. + */ + public final CoordinateReferenceSystem preferredCRS; + + /** + * The transform from {@link #preferredCRS} to the CRS specified at construction time. + * Never null but may be the identity transform. + */ + public final MathTransform preferredToSpecified; + + /** + * Searches wraparound axes in the specified CRS or its base CRS (if any). + * + * @param crs the CRS where to search for wraparound axes. + */ + public WraparoundAxesFinder(CoordinateReferenceSystem crs) { + if (crs instanceof ProjectedCRS) { + final ProjectedCRS p = (ProjectedCRS) crs; + crs = p.getBaseCRS(); // Geographic, so a wraparound axis certainly exists. + preferredToSpecified = p.getConversionFromBase().getMathTransform(); + } else { + // TODO: we should handle the case of CompoundCRS before to fallback on identity. + preferredToSpecified = MathTransforms.identity(ReferencingUtilities.getDimension(crs)); + } + preferredCRS = crs; + } + + /** + * Returns the range (maximum - minimum) of wraparound axes. For non-wraparound axes, the value is set to 0. + * The length of this array is the smallest length necessary for handing all wraparound axes. + * It may be smaller than the CRS dimension. + * + * @return periods of axes (0 for non-wraparound axes), or {@code null} if none. + */ + public double[] periods() { + double[] periods = null; + final CoordinateSystem cs = preferredCRS.getCoordinateSystem(); + for (int i = cs.getDimension(); --i >= 0;) { + final double period = WraparoundApplicator.range(cs, i); + if (period > 0) { + if (periods == null) { + periods = new double[i + 1]; + } + periods[i] = period; + } + } + return periods; + } +}