001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    http://www.geoapi.org
004 *
005 *    Copyright (C) 2008-2019 Open Geospatial Consortium, Inc.
006 *    All Rights Reserved. http://www.opengeospatial.org/ogc/legal
007 *
008 *    Permission to use, copy, and modify this software and its documentation, with
009 *    or without modification, for any purpose and without fee or royalty is hereby
010 *    granted, provided that you include the following on ALL copies of the software
011 *    and documentation or portions thereof, including modifications, that you make:
012 *
013 *    1. The full text of this NOTICE in a location viewable to users of the
014 *       redistributed or derivative work.
015 *    2. Notice of any changes or modifications to the OGC files, including the
016 *       date changes were made.
017 *
018 *    THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE
019 *    NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
020 *    TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT
021 *    THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY
022 *    PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
023 *
024 *    COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR
025 *    CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
026 *
027 *    The name and trademarks of copyright holders may NOT be used in advertising or
028 *    publicity pertaining to the software without specific, written prior permission.
029 *    Title to copyright in this software and any associated documentation will at all
030 *    times remain with copyright holders.
031 */
032package org.opengis.test.referencing;
033
034import java.util.List;
035
036import org.opengis.referencing.operation.*;
037import org.opengis.parameter.ParameterValueGroup;
038import org.opengis.referencing.crs.CoordinateReferenceSystem;
039
040import org.opengis.test.ValidatorContainer;
041import static org.opengis.test.Assert.*;
042
043
044/**
045 * Validates {@link CoordinateOperation} and related objects from the
046 * {@code org.opengis.referencing.operation} package.
047 *
048 * <p>This class is provided for users wanting to override the validation methods. When the default
049 * behavior is sufficient, the {@link org.opengis.test.Validators} static methods provide a more
050 * convenient way to validate various kinds of objects.</p>
051 *
052 * @author  Martin Desruisseaux (Geomatys)
053 * @version 3.1
054 * @since   2.2
055 */
056public class OperationValidator extends ReferencingValidator {
057    /**
058     * Creates a new validator instance.
059     *
060     * @param container  the set of validators to use for validating other kinds of objects
061     *                   (see {@linkplain #container field javadoc}).
062     */
063    public OperationValidator(final ValidatorContainer container) {
064        super(container, "org.opengis.referencing.operation");
065    }
066
067    /**
068     * For each interface implemented by the given object, invokes the corresponding
069     * {@code validate(…)} method defined in this class (if any).
070     *
071     * @param  object  the object to dispatch to {@code validate(…)} methods, or {@code null}.
072     * @return number of {@code validate(…)} methods invoked in this class for the given object.
073     */
074    public int dispatch(final CoordinateOperation object) {
075        int n = 0;
076        if (object != null) {
077            if (object instanceof Conversion)            {validate((Conversion)            object); n++;}
078            if (object instanceof Transformation)        {validate((Transformation)        object); n++;}
079            if (object instanceof ConcatenatedOperation) {validate((ConcatenatedOperation) object); n++;}
080            if (object instanceof PassThroughOperation)  {validate((PassThroughOperation)  object); n++;}
081            if (n == 0) {
082                if (object instanceof SingleOperation) {
083                    validateOperation((SingleOperation) object);
084                } else {
085                    validateCoordinateOperation(object);
086                }
087            }
088        }
089        return n;
090    }
091
092    /**
093     * Validates the given "pass through" operation.
094     *
095     * @param  object  the object to validate, or {@code null}.
096     */
097    public void validate(final PassThroughOperation object) {
098        if (object == null) {
099            return;
100        }
101        validateCoordinateOperation(object);
102        final MathTransform transform = object.getMathTransform();
103        mandatory("PassThroughOperation: shall have a MathTransform.", transform);
104
105        final CoordinateOperation operation = object.getOperation();
106        mandatory("PassThroughOperation: getOperation() is mandatory.", operation);
107        assertNotSame("PassThroughOperation: getOperation() can't be this.", object, operation);
108        dispatch(operation);
109
110        final int[] index = object.getModifiedCoordinates();
111        mandatory("PassThroughOperation: modified coordinates are mandatory.", index);
112        if (operation == null || index == null) {
113            return;
114        }
115        final int sourceDimension = transform.getSourceDimensions();
116        for (int i : index) {
117            assertBetween("PassThroughOperation: invalid modified coordinate index.", 0, sourceDimension-1, i);
118        }
119    }
120
121    /**
122     * Validates the given concatenated operation.
123     *
124     * @param  object  the object to validate, or {@code null}.
125     */
126    public void validate(final ConcatenatedOperation object) {
127        if (object == null) {
128            return;
129        }
130        validateCoordinateOperation(object);
131        final MathTransform transform = object.getMathTransform();
132        mandatory("ConcatenatedOperation: shall have a MathTransform.", transform);
133
134        final List<? extends CoordinateOperation> operations = object.getOperations();
135        mandatory("ConcatenatedOperation: shall provide a list of operations.", operations);
136        if (operations == null) {
137            return;
138        }
139        validate(operations);
140        CoordinateOperation first=null, last=null;
141        for (final CoordinateOperation single : operations) {
142            assertNotNull("ConcatenatedOperation: getOperations() can't contain null element.", single);
143            assertNotSame("ConcatenatedOperation: can't contain itself as a single element.", single, object);
144            dispatch(single);
145            if (first == null) {
146                first = single;
147            } else {
148                // Do not validate MathTransform since it is already done by dispatch(single).
149                final MathTransform lastMT =   last.getMathTransform();
150                final MathTransform thisMT = single.getMathTransform();
151                if (lastMT != null && thisMT != null) {
152                    assertEquals("ConcatenatedOperation: source dimension of a single operation " +
153                            "must match the target dimension of the previous one.",
154                            lastMT.getTargetDimensions(), thisMT.getSourceDimensions());
155                }
156                // Do not validate CRS since it is already done by dispatch(single).
157                final CoordinateReferenceSystem targetCRS =   last.getTargetCRS();
158                final CoordinateReferenceSystem sourceCRS = single.getSourceCRS();
159                if (targetCRS != null && sourceCRS != null) {
160                    assertEquals("ConcatenatedOperation: source dimension of a single operation " +
161                            "must match the target dimension of the previous one.",
162                            dimension(targetCRS), dimension(sourceCRS));
163                }
164            }
165            last = single;
166        }
167        assertNotNull("ConcatenatedOperation: shall contain at least one single operation.", last);
168        if (transform != null) {
169            final MathTransform firstMT = first.getMathTransform();
170            final MathTransform lastMT  = last .getMathTransform();
171            if (firstMT != null) {
172                assertEquals("ConcatenatedOperation: source dimension must match " +
173                        "the source dimension of the first single operation.",
174                        firstMT.getSourceDimensions(), transform.getSourceDimensions());
175            }
176            if (lastMT != null) {
177                assertEquals("ConcatenatedOperation: target dimension must match " +
178                        "the target dimension of the last single operation.",
179                        lastMT.getTargetDimensions(), transform.getTargetDimensions());
180            }
181        }
182        final CoordinateReferenceSystem sourceCRS = object.getSourceCRS();
183        final CoordinateReferenceSystem targetCRS = object.getTargetCRS();
184        final CoordinateReferenceSystem  firstCRS = first .getSourceCRS();
185        final CoordinateReferenceSystem   lastCRS = last  .getTargetCRS();
186        if (sourceCRS != null && firstCRS != null) {
187            assertSame("ConcatenatedOperation: sourceCRS must be the source " +
188                    "of the first single operation.", firstCRS, sourceCRS);
189        }
190        if (targetCRS != null && lastCRS != null) {
191            assertSame("ConcatenatedOperation: targetCRS must be the target " +
192                    "of the last single operation.", lastCRS, targetCRS);
193        }
194    }
195
196    /**
197     * Validates the given coordinate operation. This method is private because we
198     * choose to expose only non-ambiguous {@code validate} methods in public API.
199     *
200     * @param  object  the object to validate, or {@code null}.
201     */
202    private void validateCoordinateOperation(final CoordinateOperation object) {
203        if (object == null) {
204            return;
205        }
206        assertFalse("CoordinateOperation: can't be both a ConcatenatedOperation and a SingleOperation.",
207                (object instanceof ConcatenatedOperation) && (object instanceof SingleOperation));
208        validateIdentifiedObject(object);
209        container.validate(object.getScope());
210        container.validate(object.getDomainOfValidity());
211
212        final CoordinateReferenceSystem sourceCRS = object.getSourceCRS();
213        final CoordinateReferenceSystem targetCRS = object.getTargetCRS();
214        container.validate(sourceCRS);
215        container.validate(targetCRS);
216
217        // Note: MathTransform can be null in defining conversion. We will
218        // check for non-null value in more specific validation methods only.
219        final MathTransform transform = object.getMathTransform();
220        validate(transform);
221        if (transform != null) {
222            if (sourceCRS != null) {
223                assertEquals("CoordinateOperation: MathTransform source dimension must match sourceCRS dimension.",
224                        dimension(sourceCRS), transform.getSourceDimensions());
225            }
226            if (targetCRS != null) {
227                assertEquals("CoordinateOperation: MathTransform target dimension must match targetCRS dimension.",
228                        dimension(targetCRS), transform.getTargetDimensions());
229            }
230        }
231    }
232
233    /**
234     * Validates the given operation. This method is private because we choose
235     * to expose only non-ambiguous {@code validate} methods in public API.
236     *
237     * @param  object  the object to validate, or {@code null}.
238     */
239    @SuppressWarnings("UnnecessaryUnboxing")
240    private void validateOperation(final SingleOperation object) {
241        if (object == null) {
242            return;
243        }
244        validateCoordinateOperation(object);
245        assertFalse("Operation: can't be both a Conversion and a Transformation.",
246                (object instanceof Conversion) && (object instanceof Transformation));
247
248        final OperationMethod method = object.getMethod();
249        mandatory("Operation: OperationMethod is mandatory.", method);
250        if (method != null) {
251            validate(method);
252            final Integer opSourceDimension = method.getSourceDimensions();
253            final Integer opTargetDimension = method.getTargetDimensions();
254            final MathTransform transform = object.getMathTransform();
255            // Do not validate because it is already done by validateCoordinateOperation(object).
256            if (transform != null) {
257                if (opSourceDimension != null) {
258                    assertEquals("Operation: MathTransform source dimension must match OperationMethod source dimension.",
259                            opSourceDimension.intValue(), transform.getSourceDimensions());
260                }
261                if (opTargetDimension != null) {
262                    assertEquals("Operation: MathTransform target dimension must match OperationMethod target dimension.",
263                            opTargetDimension.intValue(), transform.getTargetDimensions());
264                }
265            }
266        }
267        final ParameterValueGroup parameters = object.getParameterValues();
268        mandatory("Operation: ParameterValues are mandatory.", method);
269        container.validate(parameters);
270    }
271
272    /**
273     * Validates the given conversion.
274     *
275     * @param  object  the object to validate, or {@code null}.
276     */
277    public void validate(final Conversion object) {
278        if (object == null) {
279            return;
280        }
281        validateOperation(object);
282        assertFalse("Projection: can't be both planar and conic.",
283                (object instanceof PlanarProjection) && (object instanceof ConicProjection));
284        assertFalse("Projection: can't be both planar and cylindrical.",
285                (object instanceof PlanarProjection) && (object instanceof CylindricalProjection));
286        assertFalse("Projection: can't be both cylindrical and conic.",
287                (object instanceof CylindricalProjection) && (object instanceof ConicProjection));
288
289        if (object.getMathTransform() != null) {
290            mandatory("Conversion: non-defining conversion should have a source CRS.", object.getSourceCRS());
291            mandatory("Conversion: non-defining conversion should have a target CRS.", object.getTargetCRS());
292        }
293        forbidden("Conversion: should not have operation version.", object.getOperationVersion());
294        if (object.getMathTransform() == null) {
295            forbidden("Conversion: defining conversion should not have source CRS", object.getSourceCRS());
296            forbidden("Conversion: defining conversion should not have target CRS", object.getTargetCRS());
297        }
298    }
299
300    /**
301     * Validates the given transformation.
302     *
303     * @param  object  the object to validate, or {@code null}.
304     */
305    public void validate(final Transformation object) {
306        if (object == null) {
307            return;
308        }
309        validateOperation(object);
310        mandatory("Transformation: operationVersion is a mandatory attribute.", object.getOperationVersion());
311        mandatory("Transformation: sourceCRS is a mandatory attribute.",        object.getSourceCRS());
312        mandatory("Transformation: targetCRS is a mandatory attribute.",        object.getTargetCRS());
313        mandatory("Transformation: MathTransform is a mandatory attribute.",    object.getMathTransform());
314    }
315
316    /**
317     * Validates the given operation method.
318     *
319     * @param  object  the object to validate, or {@code null}.
320     */
321    public void validate(final OperationMethod object) {
322        if (object == null) {
323            return;
324        }
325        final Integer sourceDimension = object.getSourceDimensions();
326        final Integer targetDimension = object.getTargetDimensions();
327        if (sourceDimension != null) {
328            assertStrictlyPositive("OperationMethod: source dimension must be greater than zero.", sourceDimension);
329        }
330        if (targetDimension != null) {
331            assertStrictlyPositive("OperationMethod: target dimension must be greater than zero.", targetDimension);
332        }
333        validate(object.getFormula());
334        container.validate(object.getParameters());
335        validateIdentifiedObject(object);
336    }
337
338    /**
339     * Validates the given formula.
340     *
341     * @param  object  the object to validate, or {@code null}.
342     */
343    public void validate(final Formula object) {
344        if (object == null) {
345            return;
346        }
347        container.validate(object.getFormula());
348        container.validate(object.getCitation());
349    }
350
351    /**
352     * Validates the given math transform.
353     *
354     * @param  object  the object to validate, or {@code null}.
355     */
356    public void validate(final MathTransform object) {
357        if (object == null) {
358            return;
359        }
360        final int sourceDimension = object.getSourceDimensions();
361        final int targetDimension = object.getTargetDimensions();
362        assertStrictlyPositive("MathTransform: source dimension must be greater than zero.", sourceDimension);
363        assertStrictlyPositive("MathTransform: target dimension must be greater than zero.", targetDimension);
364        if (object instanceof MathTransform1D) {
365            assertEquals("MathTransform1D: source dimension must be 1.", 1, sourceDimension);
366            assertEquals("MathTransform1D: target dimension must be 1.", 1, targetDimension);
367        }
368        if (object instanceof MathTransform2D) {
369            assertEquals("MathTransform2D: source dimension must be 2.", 2, sourceDimension);
370            assertEquals("MathTransform2D: target dimension must be 2.", 2, targetDimension);
371        }
372        if (object.isIdentity()) {
373            assertEquals("MathTransform: identity transforms must have the same source and target dimensions.",
374                    sourceDimension, targetDimension);
375        }
376    }
377
378    /**
379     * Returns the dimension of the given CRS.
380     */
381    private static int dimension(final CoordinateReferenceSystem crs) {
382        return crs.getCoordinateSystem().getDimension();
383    }
384}