001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    Copyright © 2008-2024 Open Geospatial Consortium, Inc.
004 *    http://www.geoapi.org
005 *
006 *    Licensed under the Apache License, Version 2.0 (the "License");
007 *    you may not use this file except in compliance with the License.
008 *    You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *    Unless required by applicable law or agreed to in writing, software
013 *    distributed under the License is distributed on an "AS IS" BASIS,
014 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *    See the License for the specific language governing permissions and
016 *    limitations under the License.
017 */
018package org.opengis.test.referencing;
019
020import java.util.List;
021
022import org.opengis.referencing.operation.*;
023import org.opengis.parameter.ParameterValueGroup;
024import org.opengis.referencing.crs.CoordinateReferenceSystem;
025
026import org.opengis.test.ValidatorContainer;
027import static org.junit.jupiter.api.Assertions.*;
028import static org.opengis.test.Assertions.assertBetween;
029import static org.opengis.test.Assertions.assertStrictlyPositive;
030
031
032/**
033 * Validates {@link CoordinateOperation} and related objects from the
034 * {@code org.opengis.referencing.operation} package.
035 *
036 * <p>This class is provided for users wanting to override the validation methods. When the default
037 * behavior is sufficient, the {@link org.opengis.test.Validators} static methods provide a more
038 * convenient way to validate various kinds of objects.</p>
039 *
040 * @author  Martin Desruisseaux (Geomatys)
041 * @version 3.1
042 * @since   2.2
043 */
044public class OperationValidator extends ReferencingValidator {
045    /**
046     * Creates a new validator instance.
047     *
048     * @param container  the set of validators to use for validating other kinds of objects
049     *                   (see {@linkplain #container field javadoc}).
050     */
051    public OperationValidator(final ValidatorContainer container) {
052        super(container, "org.opengis.referencing.operation");
053    }
054
055    /**
056     * For each interface implemented by the given object, invokes the corresponding
057     * {@code validate(…)} method defined in this class (if any).
058     *
059     * @param  object  the object to dispatch to {@code validate(…)} methods, or {@code null}.
060     * @return number of {@code validate(…)} methods invoked in this class for the given object.
061     */
062    public int dispatch(final CoordinateOperation object) {
063        int n = 0;
064        if (object != null) {
065            if (object instanceof Conversion)            {validate((Conversion)            object); n++;}
066            if (object instanceof Transformation)        {validate((Transformation)        object); n++;}
067            if (object instanceof PointMotionOperation)  {validate((PointMotionOperation)  object); n++;}
068            if (object instanceof ConcatenatedOperation) {validate((ConcatenatedOperation) object); n++;}
069            if (object instanceof PassThroughOperation)  {validate((PassThroughOperation)  object); n++;}
070            if (n == 0) {
071                if (object instanceof SingleOperation) {
072                    validateOperation((SingleOperation) object);
073                } else {
074                    validateCoordinateOperation(object);
075                }
076            }
077        }
078        return n;
079    }
080
081    /**
082     * Validates the given "pass through" operation.
083     *
084     * @param  object  the object to validate, or {@code null}.
085     */
086    public void validate(final PassThroughOperation object) {
087        if (object == null) {
088            return;
089        }
090        validateCoordinateOperation(object);
091        final MathTransform transform = object.getMathTransform();
092        mandatory(transform, "PassThroughOperation: shall have a MathTransform.");
093
094        final CoordinateOperation operation = object.getOperation();
095        mandatory(operation, "PassThroughOperation: getOperation() is mandatory.");
096        assertNotSame(object, operation, "PassThroughOperation: getOperation() cannot be this.");
097        dispatch(operation);
098
099        final int[] index = object.getModifiedCoordinates();
100        mandatory(index, "PassThroughOperation: modified coordinates are mandatory.");
101        if (operation == null || index == null) {
102            return;
103        }
104        final int sourceDimension = transform.getSourceDimensions();
105        for (int i : index) {
106            assertBetween(0, sourceDimension-1, i, "PassThroughOperation: invalid modified coordinate index.");
107        }
108    }
109
110    /**
111     * Validates the given concatenated operation.
112     *
113     * @param  object  the object to validate, or {@code null}.
114     */
115    public void validate(final ConcatenatedOperation object) {
116        if (object == null) {
117            return;
118        }
119        validateCoordinateOperation(object);
120        final MathTransform transform = object.getMathTransform();
121        mandatory(transform, "ConcatenatedOperation: shall have a MathTransform.");
122
123        final List<? extends CoordinateOperation> operations = object.getOperations();
124        mandatory(operations, "ConcatenatedOperation: shall provide a list of operations.");
125        if (operations == null) {
126            return;
127        }
128        validate(operations);
129        CoordinateOperation first=null, last=null;
130        for (final CoordinateOperation single : operations) {
131            assertNotNull(single, "ConcatenatedOperation: getOperations() cannot contain null element.");
132            assertNotSame(single, object, "ConcatenatedOperation: cannot contain itself as a single element.");
133            dispatch(single);
134            if (first == null) {
135                first = single;
136            } else {
137                // Do not validate MathTransform since it is already done by dispatch(single).
138                final MathTransform lastMT =   last.getMathTransform();
139                final MathTransform thisMT = single.getMathTransform();
140                if (lastMT != null && thisMT != null) {
141                    assertEquals(lastMT.getTargetDimensions(), thisMT.getSourceDimensions(),
142                            "ConcatenatedOperation: source dimension of a single operation " +
143                            "must match the target dimension of the previous one.");
144                }
145                // Do not validate CRS since it is already done by dispatch(single).
146                final CoordinateReferenceSystem targetCRS =   last.getTargetCRS();
147                final CoordinateReferenceSystem sourceCRS = single.getSourceCRS();
148                if (targetCRS != null && sourceCRS != null) {
149                    assertEquals(dimension(targetCRS), dimension(sourceCRS),
150                            "ConcatenatedOperation: source dimension of a single operation " +
151                            "must match the target dimension of the previous one.");
152                }
153            }
154            last = single;
155        }
156        assertNotNull(last, "ConcatenatedOperation: shall contain at least one single operation.");
157        if (transform != null) {
158            final MathTransform firstMT = first.getMathTransform();
159            final MathTransform lastMT  = last .getMathTransform();
160            if (firstMT != null) {
161                assertEquals(firstMT.getSourceDimensions(), transform.getSourceDimensions(),
162                        "ConcatenatedOperation: source dimension must match " +
163                        "the source dimension of the first single operation.");
164            }
165            if (lastMT != null) {
166                assertEquals(lastMT.getTargetDimensions(), transform.getTargetDimensions(),
167                        "ConcatenatedOperation: target dimension must match " +
168                        "the target dimension of the last single operation.");
169            }
170        }
171        final CoordinateReferenceSystem sourceCRS = object.getSourceCRS();
172        final CoordinateReferenceSystem targetCRS = object.getTargetCRS();
173        final CoordinateReferenceSystem  firstCRS = first .getSourceCRS();
174        final CoordinateReferenceSystem   lastCRS = last  .getTargetCRS();
175        if (sourceCRS != null && firstCRS != null) {
176            assertSame(firstCRS, sourceCRS,
177                    "ConcatenatedOperation: sourceCRS must be the source of the first single operation.");
178        }
179        if (targetCRS != null && lastCRS != null) {
180            assertSame(lastCRS, targetCRS,
181                    "ConcatenatedOperation: targetCRS must be the target of the last single operation.");
182        }
183    }
184
185    /**
186     * Validates the given coordinate operation. This method is private because we
187     * choose to expose only non-ambiguous {@code validate} methods in public API.
188     *
189     * @param  object  the object to validate, or {@code null}.
190     */
191    @SuppressWarnings("deprecation")
192    private void validateCoordinateOperation(final CoordinateOperation object) {
193        if (object == null) {
194            return;
195        }
196        assertFalse((object instanceof ConcatenatedOperation) && (object instanceof SingleOperation),
197                "CoordinateOperation: cannot be both a ConcatenatedOperation and a SingleOperation.");
198        validateIdentifiedObject(object);
199        container.validate(object.getScope());
200        container.validate(object.getDomainOfValidity());
201        validate("domain", object.getDomains(), ValidatorContainer::validate, false);
202
203        final CoordinateReferenceSystem sourceCRS = object.getSourceCRS();
204        final CoordinateReferenceSystem targetCRS = object.getTargetCRS();
205        container.validate(sourceCRS);
206        container.validate(targetCRS);
207
208        // Note: MathTransform can be null in defining conversion. We will
209        // check for non-null value in more specific validation methods only.
210        final MathTransform transform = object.getMathTransform();
211        validate(transform);
212        if (transform != null) {
213            if (sourceCRS != null) {
214                assertEquals(dimension(sourceCRS), transform.getSourceDimensions(),
215                        "CoordinateOperation: MathTransform source dimension must match sourceCRS dimension.");
216            }
217            if (targetCRS != null) {
218                assertEquals(dimension(targetCRS), transform.getTargetDimensions(),
219                        "CoordinateOperation: MathTransform target dimension must match targetCRS dimension.");
220            }
221        }
222    }
223
224    /**
225     * Validates the given operation. This method is private because we choose
226     * to expose only non-ambiguous {@code validate} methods in public API.
227     *
228     * @param  object  the object to validate, or {@code null}.
229     */
230    @SuppressWarnings("UnnecessaryUnboxing")
231    private void validateOperation(final SingleOperation object) {
232        if (object == null) {
233            return;
234        }
235        validateCoordinateOperation(object);
236        assertFalse((object instanceof Conversion) && (object instanceof Transformation),
237                "Operation: cannot be both a Conversion and a Transformation.");
238
239        final OperationMethod method = object.getMethod();
240        mandatory(method, "Operation: OperationMethod is mandatory.");
241        validate(method);
242        if (method != null) {
243            @SuppressWarnings("deprecation") final Integer opSourceDimension = method.getSourceDimensions();
244            @SuppressWarnings("deprecation") final Integer opTargetDimension = method.getTargetDimensions();
245            final MathTransform transform = object.getMathTransform();
246            // Do not validate because it is already done by validateCoordinateOperation(object).
247            if (transform != null) {
248                if (opSourceDimension != null) {
249                    assertEquals(opSourceDimension.intValue(), transform.getSourceDimensions(),
250                            "Operation: MathTransform source dimension must match OperationMethod source dimension.");
251                }
252                if (opTargetDimension != null) {
253                    assertEquals(opTargetDimension.intValue(), transform.getTargetDimensions(),
254                            "Operation: MathTransform target dimension must match OperationMethod target dimension.");
255                }
256            }
257        }
258        final ParameterValueGroup parameters = object.getParameterValues();
259        mandatory(method, "Operation: ParameterValues are mandatory.");
260        container.validate(parameters);
261    }
262
263    /**
264     * Validates the given conversion.
265     *
266     * @param  object  the object to validate, or {@code null}.
267     */
268    public void validate(final Conversion object) {
269        if (object == null) {
270            return;
271        }
272        validateOperation(object);
273        if (object.getMathTransform() == null) {
274            forbidden(object.getSourceCRS(), "Conversion: defining conversion should not have a source CRS");
275            forbidden(object.getTargetCRS(), "Conversion: defining conversion should not have a target CRS");
276        } else {
277            mandatory(object.getSourceCRS(), "Conversion: non-defining conversion should have a source CRS.");
278            mandatory(object.getTargetCRS(), "Conversion: non-defining conversion should have a target CRS.");
279        }
280        forbidden(object.getSourceEpoch(),      "Conversion: should not have a source epoch.");
281        forbidden(object.getTargetEpoch(),      "Conversion: should not have a target epoch.");
282        forbidden(object.getOperationVersion(), "Conversion: should not have operation version.");
283    }
284
285    /**
286     * Validates the given transformation.
287     *
288     * @param  object  the object to validate, or {@code null}.
289     */
290    public void validate(final Transformation object) {
291        if (object == null) {
292            return;
293        }
294        validateOperation(object);
295        mandatory(object.getSourceCRS(),        "Transformation: sourceCRS is a mandatory attribute.");
296        mandatory(object.getTargetCRS(),        "Transformation: targetCRS is a mandatory attribute.");
297        mandatory(object.getMathTransform(),    "Transformation: MathTransform is a mandatory attribute.");
298        mandatory(object.getOperationVersion(), "Transformation: operationVersion is a mandatory attribute.");
299        forbidden(object.getSourceEpoch(),      "Transformation: should not have a source epoch.");
300        forbidden(object.getTargetEpoch(),      "Transformation: should not have a target epoch.");
301    }
302
303    /**
304     * Validates the point motion operation.
305     *
306     * @param  object  the object to validate, or {@code null}.
307     * @since 3.1
308     */
309    public void validate(final PointMotionOperation object) {
310        if (object == null) {
311            return;
312        }
313        validateOperation(object);
314        CoordinateReferenceSystem sourceCRS, targetCRS;
315        mandatory(sourceCRS = object.getSourceCRS(), "PointMotionOperation: sourceCRS is a mandatory attribute.");
316        mandatory(targetCRS = object.getTargetCRS(), "PointMotionOperation: targetCRS is a mandatory attribute.");
317        if (sourceCRS != null && targetCRS != null) {
318            assertEquals(sourceCRS, targetCRS, "PointMotionOperation: sourceCRS and targetCRS shall be equal.");
319        }
320        mandatory(object.getSourceEpoch(),      "PointMotionOperation: source epoch is a mandatory attribute.");
321        mandatory(object.getTargetEpoch(),      "PointMotionOperation: target epoch is a mandatory attribute.");
322        mandatory(object.getOperationVersion(), "PointMotionOperation: operationVersion is a mandatory attribute.");
323        mandatory(object.getMathTransform(),    "PointMotionOperation: MathTransform is a mandatory attribute.");
324    }
325
326    /**
327     * Validates the given operation method.
328     *
329     * @param  object  the object to validate, or {@code null}.
330     */
331    public void validate(final OperationMethod object) {
332        if (object == null) {
333            return;
334        }
335        @SuppressWarnings("deprecation") final Integer sourceDimension = object.getSourceDimensions();
336        @SuppressWarnings("deprecation") final Integer targetDimension = object.getTargetDimensions();
337        if (sourceDimension != null) {
338            assertStrictlyPositive(sourceDimension, "OperationMethod: source dimension must be greater than zero.");
339        }
340        if (targetDimension != null) {
341            assertStrictlyPositive(targetDimension, "OperationMethod: target dimension must be greater than zero.");
342        }
343        validate(object.getFormula());
344        container.validate(object.getParameters());
345        validateIdentifiedObject(object);
346    }
347
348    /**
349     * Validates the given formula.
350     *
351     * @param  object  the object to validate, or {@code null}.
352     */
353    public void validate(final Formula object) {
354        if (object == null) {
355            return;
356        }
357        container.validate(object.getFormula());
358        container.validate(object.getCitation());
359    }
360
361    /**
362     * Validates the given math transform.
363     *
364     * @param  object  the object to validate, or {@code null}.
365     */
366    public void validate(final MathTransform object) {
367        if (object == null) {
368            return;
369        }
370        final int sourceDimension = object.getSourceDimensions();
371        final int targetDimension = object.getTargetDimensions();
372        assertStrictlyPositive(sourceDimension, "MathTransform: source dimension must be greater than zero.");
373        assertStrictlyPositive(targetDimension, "MathTransform: target dimension must be greater than zero.");
374        if (object instanceof MathTransform1D) {
375            assertEquals(1, sourceDimension, "MathTransform1D: source dimension must be 1.");
376            assertEquals(1, targetDimension, "MathTransform1D: target dimension must be 1.");
377        }
378        if (object instanceof MathTransform2D) {
379            assertEquals(2, sourceDimension, "MathTransform2D: source dimension must be 2.");
380            assertEquals(2, targetDimension, "MathTransform2D: target dimension must be 2.");
381        }
382        if (object.isIdentity()) {
383            assertEquals(sourceDimension, targetDimension,
384                    "MathTransform: identity transforms must have the same source and target dimensions.");
385        }
386    }
387
388    /**
389     * {@return the number of dimensions of the given CRS}.
390     *
391     * @param  crs  the coordinate reference system for which to get the number of dimensions.
392     */
393    private static int dimension(final CoordinateReferenceSystem crs) {
394        return crs.getCoordinateSystem().getDimension();
395    }
396}