001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    http://www.geoapi.org
004 *
005 *    Copyright (C) 2011-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;
033
034import java.util.Set;
035import java.util.Map;
036import java.util.Arrays;
037import java.util.EnumSet;
038import java.util.LinkedHashMap;
039import java.util.ServiceLoader;
040import org.opengis.geometry.DirectPosition;
041import org.opengis.referencing.operation.MathTransform;
042
043import static java.lang.StrictMath.*;
044
045
046/**
047 * A factory of various {@link ToleranceModifier} implementations.
048 *
049 * @author  Martin Desruisseaux (Geomatys)
050 * @version 3.1
051 * @since   3.1
052 */
053public strictfp final class ToleranceModifiers {
054    /**
055     * The standard length of one nautical mile, which is {@value} metres. This is the length
056     * of about one minute of arc of latitude along any meridian. This distance is used by
057     * {@linkplain ToleranceModifier#GEOGRAPHIC geographic tolerance modifiers} for converting
058     * linear units to angular units.
059     */
060    public static final double NAUTICAL_MILE = 1852;
061
062    /**
063     * An empty array of tolerance modifiers, to be returned by
064     * {@link #getImplementationSpecific(MathTransform)} in the common case where
065     * there is no specific implementation needed for a given math transform.
066     */
067    private static final ToleranceModifier[] EMPTY_ARRAY = new ToleranceModifier[0];
068
069    /**
070     * Do not allow instantiation of this class.
071     */
072    private ToleranceModifiers() {
073    }
074
075    /**
076     * Base implementation of all {@link ToleranceModifier} defined in the enclosing class.
077     */
078    strictfp static abstract class Abstract implements ToleranceModifier {
079        /** Compares this object with the given object for equality. */
080        @Override
081        public boolean equals(final Object object) {
082            return (object != null) && object.getClass() == getClass();
083        }
084
085        /** Returns a hash code value for this object. */
086        @Override
087        public int hashCode() {
088            return getClass().hashCode() ^ 434184245;
089        }
090
091        /** Returns a string representation for debugging purpose. */
092        @Override
093        public String toString() {
094            final StringBuilder buffer = new StringBuilder("ToleranceModifier.")
095                    .append(getClass().getSimpleName()).append('[');
096            toString(buffer);
097            return buffer.append(']').toString();
098        }
099
100        /** Overridden by subclasses for defining the inner part of {@code toString()}. */
101        void toString(final StringBuilder buffer) {
102            buffer.append('…');
103        }
104    }
105
106    /**
107     * Implementation of {@link ToleranceModifier#RELATIVE}.
108     */
109    strictfp static final class Relative extends Abstract {
110        @Override
111        public void adjust(final double[] tolerance, final DirectPosition coordinate, final CalculationType mode) {
112            for (int i=0; i<tolerance.length; i++) {
113                final double scale = abs(coordinate.getOrdinate(i));
114                if (scale > 1) {
115                    tolerance[i] *= scale;
116                }
117            }
118        }
119    };
120
121    /**
122     * Converts λ and φ tolerance values from metres to degrees before comparing
123     * geographic coordinates. The tolerance for the longitude (λ) and latitude (φ)
124     * ordinate values are converted from metres to degrees using the standard length of one
125     * nautical mile ({@value #NAUTICAL_MILE} metres per minute of angle). Next, the λ
126     * tolerance is adjusted according the distance of the φ ordinate value to the pole.
127     * In the extreme case where the coordinate to compare is located at a pole, then the
128     * tolerance is 360° in longitude values.
129     *
130     * @param  λDimension  the dimension of longitude ordinate values (typically 0 or 1).
131     * @param  φDimension  the dimension of latitude ordinate values (typically 0 or 1).
132     * @return a tolerance modifier suitable for comparing geographic coordinates.
133     *
134     * @see ToleranceModifier#GEOGRAPHIC
135     * @see ToleranceModifier#GEOGRAPHIC_φλ
136     */
137    public static ToleranceModifier geographic(final int λDimension, final int φDimension) {
138        if (λDimension == 0 && φDimension == 1) {
139            return ToleranceModifier.GEOGRAPHIC;
140        }
141        if (φDimension == 0 && λDimension == 1) {
142            return ToleranceModifier.GEOGRAPHIC_φλ;
143        }
144        return new Geographic(λDimension, φDimension);
145    }
146
147    /**
148     * Implementation of the value returned by {@link ToleranceModifiers#geographic(int,int)}.
149     */
150    strictfp static class Geographic extends Abstract {
151        /** The dimension of the tolerance values to modify. */
152        private final int λDimension, φDimension;
153
154        /** Invoked by the public static method or field only. */
155        Geographic(final int λDimension, final int φDimension) {
156            this.λDimension = λDimension;
157            this.φDimension = φDimension;
158            if (λDimension < 0) throw new IllegalArgumentException("Illegal λ dimension: " + λDimension);
159            if (φDimension < 0) throw new IllegalArgumentException("Illegal φ dimension: " + φDimension);
160            if (φDimension == λDimension) {
161                throw new IllegalArgumentException("λ and φ dimensions must be different.");
162            }
163        }
164
165        /** Adjusts the (λ,φ) tolerances as documented in the enclosing class. */
166        @Override
167        public void adjust(final double[] tolerance, final DirectPosition coordinate, final CalculationType mode) {
168            tolerance[φDimension] /= (NAUTICAL_MILE * 60);  // 1 nautical miles = 1852 metres in 1 minute of angle.
169            double tol = tolerance[λDimension];
170            if (tol != 0) {
171                tol /= (NAUTICAL_MILE*60 * cos(toRadians(abs(coordinate.getOrdinate(φDimension)))));
172                if (!(tol <= 360)) {                        // !(a<=b) rather than (a>b) in order to catch NaN.
173                    tol = 360;
174                }
175                tolerance[λDimension] = tol;
176            }
177        }
178
179        /** Compares this object with the given object for equality. */
180        @Override
181        public boolean equals(final Object object) {
182            if (super.equals(object)) {
183                final Geographic other = (Geographic) object;
184                return other.λDimension == λDimension && other.φDimension == φDimension;
185            }
186            return false;
187        }
188
189        /** Returns a hash code value for this object. */
190        @Override
191        public int hashCode() {
192            return (super.hashCode()*31 + λDimension)*31 + φDimension;
193        }
194
195        /** Formats λ and φ symbols at the position of their dimension. */
196        @Override
197        final void toString(final StringBuilder buffer) {
198            final int n = max(λDimension, φDimension);
199            for (int i=0; i<=n; i++) {
200                buffer.append(i == λDimension ? 'λ' : (i == φDimension ? 'φ' : '·')).append(',');
201            }
202            super.toString(buffer);
203        }
204    };
205
206    /**
207     * Converts λ and φ tolerance values from metres to degrees before comparing
208     * the result of an <cite>inverse projection</cite>. For <cite>forward projections</cite>
209     * and all other calculations, the tolerance values are left unchanged.
210     *
211     * <p>The modifier performs the work documented in {@link #geographic(int, int)} if and only if
212     * the {@link CalculationType} is {@link CalculationType#INVERSE_TRANSFORM INVERSE_TRANSFORM}.
213     * For all other cases, the modifier does nothing.</p>
214     *
215     * @param  λDimension  the dimension of longitude ordinate values (typically 0 or 1).
216     * @param  φDimension  the dimension of latitude ordinate values (typically 0 or 1).
217     * @return a tolerance modifier suitable for comparing projected coordinates.
218     *
219     * @see ToleranceModifier#PROJECTION
220     * @see ToleranceModifier#PROJECTION_FROM_φλ
221     */
222    public static ToleranceModifier projection(final int λDimension, final int φDimension) {
223        if (λDimension == 0 && φDimension == 1) {
224            return ToleranceModifier.PROJECTION;
225        }
226        if (φDimension == 0 && λDimension == 1) {
227            return ToleranceModifier.PROJECTION_FROM_φλ;
228        }
229        return new Projection(λDimension, φDimension);
230    }
231
232    /**
233     * Implementation of the value returned by {@link ToleranceModifiers#projection(int,int)}.
234     */
235    strictfp static final class Projection extends Geographic {
236        /** Invoked by the public static method or field only. */
237        Projection(final int λDimension, final int φDimension) {
238            super(λDimension, φDimension);
239        }
240
241        /** Adjusts the (λ,φ) tolerances as documented in the enclosing class. */
242        @Override
243        public void adjust(final double[] tolerance, final DirectPosition coordinate, final CalculationType mode) {
244            if (mode == CalculationType.INVERSE_TRANSFORM) {
245                super.adjust(tolerance, coordinate, mode);
246            }
247        }
248    };
249
250    /**
251     * Multiplies tolerance values by the given factors. For every dimension <var>i</var>, this
252     * modifier multiplies <code>tolerance[<var>i</var>]</code> by <code>factors[<var>i</var>]</code>.
253     *
254     * <p>If the tolerance array is longer than the factors array, all extra tolerance values are left
255     * unchanged. If the tolerance array is shorter than the factors array, the extra factors values
256     * are ignored.</p>
257     *
258     * @param  types    the calculation types for which to apply the given scale factors.
259     * @param  factors  the factors by which to multiply the tolerance values.
260     * @return a tolerance modifier that scale the tolerance thresholds, or {@code null} if
261     *         the given set or array is empty or all the given scale factors are equals to 1.
262     */
263    public static ToleranceModifier scale(Set<CalculationType> types, final double... factors) {
264        types = EnumSet.copyOf(types);
265        if (types.isEmpty()) {
266            return null;
267        }
268        int upper = 0;
269        for (int i=0; i<factors.length;) {
270            final double factor = factors[i];
271            if (!(factor >= 0)) { // !(a>=0) instead than (a<0) for catching NaN.
272                throw new IllegalArgumentException("Illegal scale: factors[" + i + "] = " + factor);
273            }
274            i++;
275            if (factor != 1) {
276                upper = i;
277            }
278        }
279        return (upper != 0) ? new Scale(types, Arrays.copyOf(factors, upper)) : null;
280    }
281
282    /**
283     * Implementation of the value returned by {@link ToleranceModifiers#scale(double[])}.
284     */
285    strictfp static final class Scale extends Abstract {
286        /** The types for which to apply the scale factors. */
287        private final Set<CalculationType> types;
288
289        /** The scale factors. */
290        private final double[] factors;
291
292        /** Invoked by the public static method only. */
293        Scale(final Set<CalculationType> types, final double[] factors) {
294            this.types   = types;
295            this.factors = factors;
296        }
297
298        /** Gets the scaled tolerance threshold as documented in the enclosing class. */
299        @Override
300        public void adjust(final double[] tolerance, final DirectPosition coordinate, final CalculationType mode) {
301            if (types.contains(mode)) {
302                for (int i=min(tolerance.length, factors.length); --i>=0;) {
303                    tolerance[i] *= factors[i];
304                }
305            }
306        }
307
308        /** Compares this object with the given object for equality. */
309        @Override
310        public boolean equals(final Object object) {
311            if (super.equals(object)) {
312                final Scale other = (Scale) object;
313                return types.equals(other.types) && Arrays.equals(factors, other.factors);
314            }
315            return false;
316        }
317
318        /** Returns a hash code value for this object. */
319        @Override
320        public int hashCode() {
321            return super.hashCode() + 31*(types.hashCode() + 31*Arrays.hashCode(factors));
322        }
323
324        /** Appends the scale factors. */
325        @Override
326        void toString(final StringBuilder buffer) {
327            boolean more = false;
328            for (final CalculationType type : types) {
329                if (more) buffer.append(',');
330                buffer.append(type);
331                more = true;
332            }
333            buffer.append(':');
334            for (final double factor : factors) {
335                if (factor == 1) {
336                    buffer.append('·');
337                } else {
338                    buffer.append('×');
339                    final int casted = (int) factor;
340                    if (casted == factor) {
341                        buffer.append(casted);
342                    } else {
343                        buffer.append(factor);
344                    }
345                }
346                buffer.append(',');
347            }
348            super.toString(buffer);
349        }
350    }
351
352    /**
353     * Returns a modifier which will return the maximal tolerance threshold of all the given
354     * modifiers for each dimension.
355     *
356     * @param  modifiers  the modifiers to iterate over.
357     * @return a filter for the maximal tolerance threshold of all the given modifiers,
358     *         or {@code null} if the given {@code modifiers} array is empty.
359     */
360    public static ToleranceModifier maximum(final ToleranceModifier... modifiers) {
361        final int length = modifiers.length;
362        switch (length) {
363            case 0:  return null;
364            case 1:  return modifiers[0];
365        }
366        /*
367         * If any element of the given modifiers array is an other instance of the
368         * 'Maximum' modifier, concatenate all modifier arrays into a single array.
369         */
370        ToleranceModifier[] expanded = new ToleranceModifier[length];
371        for (int i=0,t=0; i<length; i++) {
372            final ToleranceModifier modifier = modifiers[i];
373            if (modifier == null) {
374                throw new NullPointerException("modifiers[" + i + "] is null.");
375            }
376            if (modifier instanceof Maximum) {
377                final ToleranceModifier[] insert = ((Maximum) modifier).modifiers;
378                expanded = Arrays.copyOf(expanded, expanded.length + insert.length-1);
379                System.arraycopy(insert, 0, expanded, t, insert.length);
380                t += insert.length;
381            } else {
382                expanded[t++] = modifier;
383            }
384        }
385        return new Maximum(expanded);
386    }
387
388    /**
389     * The implementation of {@link ToleranceModifiers#maximum(ToleranceModifier[])}.
390     */
391    private strictfp static final class Maximum extends Abstract {
392        /** The modifiers from which to get the maximal tolerance. */
393        private final ToleranceModifier[] modifiers;
394
395        /** Invoked by the public static method only. */
396        Maximum(final ToleranceModifier[] modifiers) {
397            this.modifiers = modifiers;
398        }
399
400        /** Gets the maximal tolerance threshold as documented in the enclosing class. */
401        @Override
402        public void adjust(final double[] tolerance, final DirectPosition coordinate, final CalculationType mode) {
403            final double[] original = tolerance.clone();
404            final double[] copy = new double[original.length];
405            for (final ToleranceModifier modifier : modifiers) {
406                System.arraycopy(original, 0, copy, 0, original.length);
407                modifier.adjust(copy, coordinate, mode);
408                for (int i=0; i<copy.length; i++) {
409                    final double tol = copy[i];
410                    if (tol > tolerance[i]) {
411                        tolerance[i] = tol;
412                    }
413                }
414            }
415        }
416
417        /** Compares this object with the given object for equality. */
418        @Override
419        public boolean equals(final Object object) {
420            return super.equals(object) && Arrays.equals(((Maximum) object).modifiers, modifiers);
421        }
422
423        /** Returns a hash code value for this object. */
424        @Override
425        public int hashCode() {
426            return super.hashCode() ^ Arrays.hashCode(modifiers);
427        }
428
429        /** Concatenates the string representations of the enclosed modifiers. */
430        @Override
431        void toString(final StringBuilder buffer) {
432            Concatenate.toString(buffer, ", ", modifiers);
433        }
434    }
435
436    /**
437     * Returns a concatenation of two existing modifiers. The tolerance threshold are first
438     * adjusted according the first modifier, then the result is given to the second modifier
439     * for an additional adjustment.
440     *
441     * @param  first   the first modifier, or {@code null}.
442     * @param  second  the second modifier, or {@code null}.
443     * @return the concatenation of the two given identifiers, or {@code null} if both identifiers are {@code null}.
444     */
445    public static ToleranceModifier concatenate(final ToleranceModifier first, final ToleranceModifier second) {
446        if (first  == null) return second;
447        if (second == null) return first;
448        return new Concatenate(first, second);
449    }
450
451    /**
452     * The implementation of {@link ToleranceModifiers#concatenate(ToleranceModifier, ToleranceModifier)}.
453     */
454    private strictfp static final class Concatenate extends Abstract {
455        /** The modifiers to concatenate. */
456        private final ToleranceModifier first, second;
457
458        /** Invoked by the public static method only. */
459        Concatenate(final ToleranceModifier first, final ToleranceModifier second) {
460            this.first  = first;
461            this.second = second;
462        }
463
464        /** Gets the concatenated threshold as documented in the enclosing class. */
465        @Override
466        public void adjust(final double[] tolerance, final DirectPosition coordinate, final CalculationType mode) {
467            first .adjust(tolerance, coordinate, mode);
468            second.adjust(tolerance, coordinate, mode);
469        }
470
471        /** Compares this object with the given object for equality. */
472        @Override
473        public boolean equals(final Object object) {
474            if (super.equals(object)) {
475                final Concatenate other = (Concatenate) object;
476                return first.equals(other.first) && second.equals(other.second);
477            }
478            return false;
479        }
480
481        /** Returns a hash code value for this object. */
482        @Override
483        public int hashCode() {
484            return (super.hashCode()*31 + first.hashCode())*31 + second.hashCode();
485        }
486
487        /** Concatenates the string representations of the enclosed modifiers. */
488        @Override
489        void toString(final StringBuilder buffer) {
490            toString(buffer, " → ", first, second);
491        }
492
493        /** Concatenates the string representations of the given modifiers. */
494        static void toString(final StringBuilder buffer, final String separator, final ToleranceModifier... modifiers) {
495            String next = "";
496            for (final ToleranceModifier modifier : modifiers) {
497                String st = modifier.toString();
498                if (modifier instanceof Abstract) {
499                    st = st.substring(st.indexOf('.') + 1);
500                }
501                buffer.append(next).append(st);
502                next = separator;
503            }
504        }
505    }
506
507    /**
508     * Returns all implementation-specific modifiers found on the classpath for the given math
509     * transform. Implementors can modify the tolerance threshold for particular math transforms
510     * using the {@link ImplementationDetails} interface.
511     *
512     * @param  transform  the transform for which to get implementation-specific modifiers.
513     * @return all implementation-specific modifiers found, or an empty array if none.
514     *
515     * @see ImplementationDetails#tolerance(MathTransform)
516     */
517    public static ToleranceModifier[] getImplementationSpecific(final MathTransform transform) {
518        Map<ToleranceModifier,Boolean> modifiers = null;
519        final ServiceLoader<ImplementationDetails> services = TestCase.getImplementationDetails();
520        synchronized (services) {
521            for (final ImplementationDetails impl : services) {
522                final ToleranceModifier modifier = impl.tolerance(transform);
523                if (modifier != null) {
524                    if (modifiers == null) {
525                        modifiers = new LinkedHashMap<>();
526                    }
527                    modifiers.put(modifier, null);
528                }
529            }
530        }
531        return (modifiers != null) ? modifiers.keySet().toArray(new ToleranceModifier[modifiers.size()]) : EMPTY_ARRAY;
532    }
533}