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