blob: c409d60631d1d523b40c1a4440a53246198b5d83 [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.function.Predicate;
import javax.measure.Unit;
import org.opengis.util.CodeList;
import org.opengis.util.GenericName;
import org.opengis.referencing.datum.VerticalDatumType;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.cs.AxisDirection;
import org.apache.sis.util.StringBuilders;
import org.apache.sis.measure.Units;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Characters;
/**
* Extensions to the standard set of {@link VerticalDatumType}.
* Those constants are not in public API because they were intentionally omitted from ISO 19111,
* and the ISO experts said that they should really not be public.
*
* <p>This class implements {@link Predicate} for opportunist reasons.
* This implementation convenience may change in any future SIS version.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
* @since 0.4
* @module
*/
public final class VerticalDatumTypes implements Predicate<CodeList<?>> {
/**
* A vertical datum for ellipsoidal heights that are measured along the
* normal to the ellipsoid used in the definition of horizontal datum.
*
* <p>Identifier: {@code CS_DatumType.CS_VD_Ellipsoidal}</p>
*/
public static final VerticalDatumType ELLIPSOIDAL = VerticalDatumType.valueOf("ELLIPSOIDAL");
/**
* A vertical datum for orthometric heights that are measured along the plumb line.
*
* <p>Identifier: {@code CS_DatumType.CS_VD_Orthometric}</p>
*/
public static final VerticalDatumType ORTHOMETRIC = VerticalDatumType.valueOf("ORTHOMETRIC");
/**
* Mapping from the numeric values used in legacy specification (OGC 01-009) to {@link VerticalDatumType}.
* Indices in this array are the legacy codes minus 2000.
*
* <strong>This array shall not be fill before the above static constants.</strong>
*/
private static final VerticalDatumType[] TYPES;
/**
* Mapping from {@link VerticalDatumType} to the numeric values used in legacy specification (OGC 01-009).
*/
private static final short[] LEGACY_CODES;
static {
TYPES = new VerticalDatumType[7];
LEGACY_CODES = new short[Math.max(ELLIPSOIDAL.ordinal(), ORTHOMETRIC.ordinal()) + 1];
for (short code = 2000; code <= 2006; code++) {
final VerticalDatumType type;
switch (code) {
case 2000: type = VerticalDatumType .OTHER_SURFACE; break; // CS_VD_Other
case 2001: type = VerticalDatumTypes.ORTHOMETRIC; break; // CS_VD_Orthometric
case 2002: type = VerticalDatumTypes.ELLIPSOIDAL; break; // CS_VD_Ellipsoidal
case 2003: type = VerticalDatumType .BAROMETRIC; break; // CS_VD_AltitudeBarometric
case 2005: type = VerticalDatumType .GEOIDAL; break; // CS_VD_GeoidModelDerived
case 2006: type = VerticalDatumType .DEPTH; break; // CS_VD_Depth
default: continue;
}
TYPES[code - 2000] = type;
LEGACY_CODES[type.ordinal()] = code;
}
}
/**
* Returns the vertical datum type from a legacy code. The legacy codes were defined in
* <a href="http://www.opengeospatial.org/standards/ct">OGC 01-009</a>
* (<cite>Coordinate Transformation Services)</cite>, which also defined the version 1 of
* <a href="http://www.geoapi.org/3.0/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
* Known Text</cite></a> format (WKT 1). This method is used for WKT 1 parsing.
*
* @param code the legacy vertical datum code.
* @return the vertical datum type, or {@code null} if the code is unrecognized.
*/
public static VerticalDatumType fromLegacy(int code) {
code -= 2000;
return (code >= 0 && code < TYPES.length) ? TYPES[code] : null;
}
/**
* Returns the legacy code for the datum type, or 0 if unknown.
* This method is used for WKT 1 formatting.
*
* @param type the vertical datum type, or {@code null} if unknown.
* @return the legacy code for the given datum type, or 0 if unknown.
*/
public static int toLegacy(final VerticalDatumType type) {
if (type != null) {
final int ordinal = type.ordinal();
if (ordinal >= 0 && ordinal < LEGACY_CODES.length) {
return LEGACY_CODES[ordinal];
}
}
return 0;
}
/**
* Guesses the type of a datum from its name, aliases or a given vertical axis. This is sometime needed
* after XML unmarshalling or WKT parsing, since GML 3.2 and ISO 19162 do not contain any attribute for
* the datum type.
*
* <p>This method uses heuristic rules and may be changed in any future SIS version.
* If the type can not be determined, defaults to {@link VerticalDatumType#OTHER_SURFACE}.</p>
*
* @param name the name of the datum for which to guess a type, or {@code null} if unknown.
* @param aliases the aliases of the datum for which to guess a type, or {@code null} if unknown.
* @param axis the vertical axis for which to guess a type, or {@code null} if unknown.
* @return a datum type, or {@link VerticalDatumType#OTHER_SURFACE} if none can be guessed.
*/
public static VerticalDatumType guess(final String name, final Collection<? extends GenericName> aliases,
final CoordinateSystemAxis axis)
{
VerticalDatumType type = guess(name);
if (type != null) {
return type;
}
if (aliases != null) {
for (final GenericName alias : aliases) {
type = guess(alias.tip().toString());
if (type != null) {
return type;
}
}
}
if (axis != null) {
final Unit<?> unit = axis.getUnit();
if (Units.isLinear(unit)) {
final String abbreviation = axis.getAbbreviation();
if (abbreviation.length() == 1) {
AxisDirection dir = AxisDirection.UP; // Expected direction for accepting the type.
switch (abbreviation.charAt(0)) {
case 'h': type = ELLIPSOIDAL; break;
case 'H': type = VerticalDatumType.GEOIDAL; break;
case 'D': type = VerticalDatumType.DEPTH; dir = AxisDirection.DOWN; break;
default: return VerticalDatumType.OTHER_SURFACE;
}
if (dir.equals(axis.getDirection())) {
return type;
}
}
} else if (Units.isPressure(unit)) {
return VerticalDatumType.BAROMETRIC;
}
}
return VerticalDatumType.OTHER_SURFACE;
}
/**
* Guesses the type of a datum of the given name. This method attempts to guess only if the given name
* contains at least one letter. If the type can not be determined, returns {@code null}.
*
* @param name name of the datum for which to guess a type, or {@code null}.
* @return a datum type, or {@code null} if none can be guessed.
*/
private static VerticalDatumType guess(final String name) {
if (name != null) {
if (CharSequences.equalsFiltered("Mean Sea Level", name, Characters.Filter.LETTERS_AND_DIGITS, true)) {
return VerticalDatumType.GEOIDAL;
}
for (int i=0; i<name.length();) {
final int c = name.codePointAt(i);
if (Character.isLetter(c)) {
return CodeList.valueOf(VerticalDatumType.class, new VerticalDatumTypes(name), null);
}
i += Character.charCount(c);
}
}
return null;
}
/**
* The name of a datum to compare against the list of datum types,
* in upper-case and (if possible) using US-ASCII characters.
*/
private final StringBuilder datum;
/**
* Creates a new {@code CodeList.Filter} which will compare the given datum name against the list of datum types.
* The comparison is case-insensitive and ignores some non-ASCII characters. The exact algorithm applied here is
* implementation dependent and may change in any future version.
*
* @param name the datum name.
*/
private VerticalDatumTypes(final String name) {
final int length = name.length();
datum = new StringBuilder(length);
for (int i=0; i<length;) {
final int c = name.codePointAt(i);
datum.appendCodePoint(Character.toUpperCase(c));
i += Character.charCount(c);
}
StringBuilders.toASCII(datum);
}
/**
* Returns {@code true} if the name of the given code is the prefix of a word in the datum name.
* We do not test the characters following the prefix because the word may be incomplete
* (e.g. {@code "geoid"} versus {@code "geoidal"}).
*
* <p>This method is public as an implementation side-effect and should be ignored.</p>
*
* @param code the code to test.
* @return {@code true} if the code matches the criterion.
*/
@Override
public boolean test(final CodeList<?> code) {
final int i = datum.indexOf(code.name());
return (i == 0) || (i >= 0 && Character.isWhitespace(datum.codePointBefore(i)));
}
}