blob: 9622a3d26d4f7bce1e0f89521b1f3ec638a14f5d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.internal.referencing;
import java.util.Collection;
import java.util.Collections;
import java.io.ObjectStreamException;
import javax.xml.bind.annotation.XmlTransient;
import javax.measure.quantity.Length;
import javax.measure.Unit;
import org.opengis.util.Record;
import org.opengis.util.InternationalString;
import org.opengis.metadata.quality.PositionalAccuracy;
import org.opengis.metadata.quality.EvaluationMethodType;
import org.opengis.metadata.quality.QuantitativeResult;
import org.opengis.metadata.quality.Result;
import org.opengis.referencing.operation.ConcatenatedOperation;
import org.opengis.referencing.operation.Conversion;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.Transformation;
import org.apache.sis.measure.Units;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.metadata.iso.quality.DefaultConformanceResult;
import org.apache.sis.metadata.iso.quality.DefaultAbsoluteExternalPositionalAccuracy;
import org.apache.sis.util.resources.Vocabulary;
/**
* Pre-defined positional accuracy resulting from some coordinate operations.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
*
* @see org.opengis.referencing.operation.Transformation#getCoordinateOperationAccuracy()
*
* @since 0.5
* @module
*/
@XmlTransient
public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPositionalAccuracy {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -2554090935254116470L;
/**
* Presumed worst case error when no datum shift information was found.
* The highest value found in the EPSG database 6.7 is 999 metres (worst datum shift), so this error
* should be yet higher. I have seen 3 kilometres mentioned in some documentation somewhere.
*
* <p>If this value is modified, please update {@code getLinearAccuracy()} public javadoc accordingly.</p>
*
* @see org.apache.sis.referencing.operation.AbstractCoordinateOperation#getLinearAccuracy()
*/
public static final double UNKNOWN_ACCURACY = 3000;
/**
* Default accuracy of datum shift, if not explicitly provided in the EPSG database.
* The 25 meters value is the next highest value (after 999 metres) found in the EPSG
* database version 6.7 for a significant number of transformations.
*
* <p>If this value is modified, please update {@code getLinearAccuracy()} public javadoc accordingly.</p>
*
* @see org.apache.sis.referencing.operation.AbstractCoordinateOperation#getLinearAccuracy()
*/
private static final double DATUM_SHIFT_ACCURACY = 25;
/**
* Default accuracy of datum shifts when using an intermediate datum (typically WGS 84).
* Since this is a concatenation of two datum shifts, we use twice {@link #DATUM_SHIFT_ACCURACY}.
* The result is multiplied by 2 again as a margin because we have no guarantees that the domain
* of validity of the two datum are close enough for making this concatenation valid.
*/
public static final double INDIRECT_SHIFT_ACCURACY = 100;
/**
* Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
* requires a datum shift and some method has been applied. Datum shift methods often use
* {@linkplain org.apache.sis.referencing.datum.BursaWolfParameters Bursa Wolf parameters},
* but other kind of method may have been applied as well.
*/
public static final PositionalAccuracy DATUM_SHIFT_APPLIED;
/**
* Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
* requires a datum shift, but no method has been found applicable. This usually means that no
* {@linkplain org.apache.sis.referencing.datum.BursaWolfParameters Bursa Wolf parameters} have
* been found. Such datum shifts are approximations and may have 1 kilometer error.
*/
public static final PositionalAccuracy DATUM_SHIFT_OMITTED;
/**
* Indicates that a {@linkplain org.opengis.referencing.operation.Transformation transformation}
* requires a datum shift, but only an indirect method has been found. The indirect method uses
* an intermediate datum, typically WGS 84.
*/
public static final PositionalAccuracy INDIRECT_SHIFT_APPLIED;
static {
final InternationalString desc = Vocabulary.formatInternational(Vocabulary.Keys.TransformationAccuracy);
final InternationalString eval = Resources .formatInternational(Resources.Keys.ConformanceMeansDatumShift);
DATUM_SHIFT_APPLIED = new PositionalAccuracyConstant(desc, eval, true);
DATUM_SHIFT_OMITTED = new PositionalAccuracyConstant(desc, eval, false);
INDIRECT_SHIFT_APPLIED = new PositionalAccuracyConstant(desc, eval, true);
}
/**
* Creates an positional accuracy initialized to the given result.
*/
private PositionalAccuracyConstant(final InternationalString measureDescription,
final InternationalString evaluationMethodDescription, final boolean pass)
{
DefaultConformanceResult result = new DefaultConformanceResult(Citations.SIS, evaluationMethodDescription, pass);
setResults(Collections.singleton(result));
setMeasureDescription(measureDescription);
setEvaluationMethodDescription(evaluationMethodDescription);
setEvaluationMethodType(EvaluationMethodType.DIRECT_INTERNAL);
transitionTo(State.FINAL);
}
/**
* Invoked on deserialization. Replace this instance by one of the constants, if applicable.
*
* @return the object to use after deserialization.
* @throws ObjectStreamException if the serialized object defines an unknown data type.
*/
private Object readResolve() throws ObjectStreamException {
if (equals(DATUM_SHIFT_APPLIED)) return DATUM_SHIFT_APPLIED;
if (equals(DATUM_SHIFT_OMITTED)) return DATUM_SHIFT_OMITTED;
if (equals(INDIRECT_SHIFT_APPLIED)) return INDIRECT_SHIFT_APPLIED;
return this;
}
/**
* Convenience method returning the accuracy in meters for the specified operation.
* This method tries each of the following procedures and returns the first successful one:
*
* <ul>
* <li>If at least one {@link QuantitativeResult} is found with a linear unit, then the largest
* accuracy estimate is converted to {@linkplain Units#METRE metres} and returned.</li>
* <li>Otherwise, if the operation is a {@link Conversion}, then returns 0 since a conversion
* is by definition accurate up to rounding errors.</li>
* <li>Otherwise, if the operation is a {@link Transformation}, then checks if the datum shift
* were applied with the help of Bursa-Wolf parameters. This procedure looks for SIS-specific
* {@link #DATUM_SHIFT_APPLIED} and {@link #DATUM_SHIFT_OMITTED DATUM_SHIFT_OMITTED} constants.</li>
* <li>Otherwise, if the operation is a {@link ConcatenatedOperation}, returns the sum of the accuracy
* of all components. This is a conservative scenario where we assume that errors cumulate linearly.
* Note that this is not necessarily the "worst case" scenario since the accuracy could be worst
* if the math transforms are highly non-linear.</li>
* </ul>
*
* If the above is modified, please update {@code AbstractCoordinateOperation.getLinearAccuracy()} javadoc.
*
* @param operation the operation to inspect for accuracy.
* @return the accuracy estimate (always in meters), or NaN if unknown.
*
* @see org.apache.sis.referencing.operation.AbstractCoordinateOperation#getLinearAccuracy()
*/
public static double getLinearAccuracy(final CoordinateOperation operation) {
double accuracy = Double.NaN;
final Collection<PositionalAccuracy> accuracies = operation.getCoordinateOperationAccuracy();
for (final PositionalAccuracy metadata : accuracies) {
for (final Result result : metadata.getResults()) {
if (result instanceof QuantitativeResult) {
final QuantitativeResult quantity = (QuantitativeResult) result;
final Collection<? extends Record> records = quantity.getValues();
if (records != null) {
final Unit<?> unit = quantity.getValueUnit();
if (Units.isLinear(unit)) {
final Unit<Length> unitOfLength = unit.asType(Length.class);
for (final Record record : records) {
for (final Object value : record.getAttributes().values()) {
if (value instanceof Number) {
double v = ((Number) value).doubleValue();
v = unitOfLength.getConverterTo(Units.METRE).convert(v);
if (v >= 0 && !(v <= accuracy)) { // '!' is for replacing the NaN value.
accuracy = v;
}
}
}
}
}
}
}
}
}
if (Double.isNaN(accuracy)) {
/*
* No quantitative (linear) accuracy were found. If the coordinate operation is actually
* a conversion, the accuracy is up to rounding error (i.e. conceptually 0) by definition.
*/
if (operation instanceof Conversion) {
return 0;
}
/*
* If the coordinate operation is actually a transformation, checks if Bursa-Wolf parameters
* were available for the datum shift. This is SIS-specific. See field javadoc for a rational
* about the return values chosen.
*/
if (operation instanceof Transformation) {
for (final PositionalAccuracy element : accuracies) {
/*
* Really need identity comparisons, not Object.equals(Object), because the later
* does not distinguish between DATUM_SHIFT_APPLIED and INDIRECT_SHIFT_APPLIED.
*/
if (element == DATUM_SHIFT_APPLIED) return DATUM_SHIFT_ACCURACY;
if (element == DATUM_SHIFT_OMITTED) return UNKNOWN_ACCURACY;
if (element == INDIRECT_SHIFT_APPLIED) return INDIRECT_SHIFT_ACCURACY;
}
}
/*
* If the coordinate operation is a compound of other coordinate operations, returns the sum of their accuracy,
* skipping unknown ones. Making the sum is a conservative approach (not exactly the "worst case" scenario,
* since it could be worst if the transforms are highly non-linear).
*/
if (operation instanceof ConcatenatedOperation) {
for (final CoordinateOperation op : ((ConcatenatedOperation) operation).getOperations()) {
final double candidate = Math.abs(getLinearAccuracy(op));
if (!Double.isNaN(candidate)) {
if (Double.isNaN(accuracy)) {
accuracy = candidate;
} else {
accuracy += candidate;
}
}
}
}
}
return accuracy;
}
}