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}