blob: e106ef91c8fe858ad08686a22fe12ad5786c3198 [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.referencing.cs;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.opengis.referencing.cs.AxisDirection;
import org.apache.sis.util.iso.Types;
import org.apache.sis.measure.Units;
import org.apache.sis.measure.Longitude;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.internal.referencing.AxisDirections;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.system.Modules;
import org.apache.sis.io.wkt.FormattableObject;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
/**
* Parses {@linkplain AxisDirection axis direction} of the kind <cite>"South along 90 deg East"</cite>.
* Those directions are used in the EPSG database for polar stereographic projections.
*
* <h2>Reference meridian</h2>
* This class does not know whether the meridian is relative to Greenwich or any other reference meridian.
* The reference meridian shall be inferred from the geodetic datum of the {@code GeographicCRS} instance
* that contains (through its coordinate system) the axes having those directions. This is consistent with
* ISO 19162:2015 §7.5.4(iv) - WKT 2 formatting.
*
* <h2>Immutability and thread safety</h2>
* This final class is immutable and thus inherently thread-safe.
*
* @author Martin Desruisseaux (IRD)
* @version 0.8
* @since 0.4
* @module
*/
final class DirectionAlongMeridian extends FormattableObject implements Comparable<DirectionAlongMeridian> {
/**
* A parser for EPSG axis names. Examples:
*
* <ul>
* <li><cite>"South along 180 deg"</cite></li>
* <li><cite>"South along 90 deg East"</cite></li>
* </ul>
*/
private static final Pattern EPSG = Pattern.compile(
"(\\p{Graph}+)\\s+along\\s+([\\-\\p{Digit}\\.]+)(?:\\s+deg|\\s*°)\\s*(\\p{Graph}+)?",
Pattern.CASE_INSENSITIVE);
/**
* The base directions we are interested in.
* Any direction not in this group will be rejected by our parser.
*/
private static final AxisDirection[] NORTH_SOUTH = {AxisDirection.NORTH, AxisDirection.SOUTH},
EAST_WEST = {AxisDirection.EAST, AxisDirection.WEST};
/**
* The direction. Will be created only when first needed.
*
* @see #getDirection()
*/
private transient AxisDirection direction;
/**
* The base direction, which must be {@link AxisDirection#NORTH} or {@link AxisDirection#SOUTH}.
*/
public final AxisDirection baseDirection;
/**
* The meridian in degrees, relative to a unspecified (usually Greenwich) prime meridian.
* Meridians in the East hemisphere are positive and meridians in the West hemisphere are negative.
*/
public final double meridian;
/**
* Creates a direction.
*
* @param baseDirection the base direction, which must be {@link AxisDirection#NORTH} or {@link AxisDirection#SOUTH}.
* @param meridian the meridian in degrees, relative to a unspecified (usually Greenwich) prime meridian.
* Meridians in the East hemisphere are positive and meridians in the West hemisphere are negative.
*/
DirectionAlongMeridian(final AxisDirection baseDirection, final double meridian) {
ArgumentChecks.ensureNonNull("baseDirection", baseDirection);
if (!AxisDirection.NORTH.equals(baseDirection) && !AxisDirection.SOUTH.equals(baseDirection)) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgumentValue_2, "baseDirection", baseDirection));
}
ArgumentChecks.ensureBetween("meridian", Longitude.MIN_VALUE, Longitude.MAX_VALUE, meridian);
this.baseDirection = baseDirection;
this.meridian = meridian;
}
/**
* Returns the direction along meridian for the specified axis direction, or {@code null} if none.
*
* <p>TIP: caller can check {@link AxisDirections#isUserDefined(AxisDirection)} before to invoke this method
* for avoiding {@code DirectionAlongMeridian} initialization in the common case where it is not needed.</p>
*/
public static DirectionAlongMeridian parse(final AxisDirection direction) {
final DirectionAlongMeridian candidate;
try {
candidate = parse(direction.name());
} catch (IllegalArgumentException e) {
Logging.recoverableException(Logging.getLogger(Modules.REFERENCING), DirectionAlongMeridian.class, "parse", e);
return null;
}
if (candidate != null) {
candidate.direction = direction;
}
return candidate;
}
/**
* If the specified name is a direction along some specific meridian,
* returns information about that. Otherwise returns {@code null}.
*
* @param name the name to parse.
* @return the parsed name, or {@code null} if it is not a direction along a meridian.
* @throws IllegalArgumentException if the given name looks like a direction along a meridian,
* but an error occurred during parsing.
*/
public static DirectionAlongMeridian parse(final String name) throws IllegalArgumentException {
final Matcher m = EPSG.matcher(name);
if (!m.matches()) {
// Not the expected pattern.
return null;
}
final AxisDirection baseDirection = AxisDirections.find(m.group(1), NORTH_SOUTH);
if (baseDirection == null) { // We require "North" or "South" direction.
return null;
}
double meridian = Double.parseDouble(m.group(2));
final String group = m.group(3);
if (group != null) {
final AxisDirection sgn = AxisDirections.find(group, EAST_WEST);
if (sgn == null) { // We require "East" or "West" direction.
return null;
}
if (sgn != AxisDirections.absolute(sgn)) {
meridian = -meridian;
}
}
return new DirectionAlongMeridian(baseDirection, meridian);
}
/**
* Returns the axis direction for this object. If a suitable axis direction already exists,
* it will be returned. Otherwise a new one is created and returned.
*/
public AxisDirection getDirection() {
if (direction == null) {
final String name = toString();
direction = AxisDirections.valueOf(name);
if (direction == null) {
direction = Types.forCodeName(AxisDirection.class, name, true);
}
}
return direction;
}
/**
* Returns the arithmetic (counterclockwise) angle from this direction to the specified direction,
* in degrees. This method returns a value between -180° and +180°, or {@link Double#NaN} if the
* {@linkplain #baseDirection base directions} don't match.
* A positive angle denote a right-handed system.
*
* <div class="note"><b>Example:</b>
* The angle from <cite>"North along 90 deg East"</cite> to <cite>"North along 0 deg</cite> is 90°.</div>
*/
public double angle(final DirectionAlongMeridian other) {
if (!baseDirection.equals(other.baseDirection)) {
return Double.NaN;
}
/*
* Example: angle between (NORTH along 90°E, NORTH along 0°) shall be +90°
*/
double angle = Longitude.normalize(meridian - other.meridian);
/*
* Reverses the sign for axis oriented toward SOUTH,
* so a positive angle is a right-handed system.
*/
if (AxisDirections.isOpposite(baseDirection)) {
angle = -angle;
}
return angle;
}
/**
* Compares this direction with the specified one for order. This method tries to reproduce
* the ordering used for the majority of coordinate systems in the EPSG database, i.e. the
* ordering of a right-handed coordinate system. Examples of ordered pairs that we should
* get (extracted from the EPSG database):
*
* <table class="sis">
* <caption>Direction name examples</caption>
* <tr><td>North along 90° East,</td> <td>North along 0°</td></tr>
* <tr><td>North along 75° West,</td> <td>North along 165° West</td></tr>
* <tr><td>South along 90° West,</td> <td>South along 0°</td></tr>
* <tr><td>South along 180°,</td> <td>South along 90° West</td></tr>
* <tr><td>North along 130° West</td> <td>North along 140° East</td></tr>
* </table>
*/
@Override
public int compareTo(final DirectionAlongMeridian that) {
final int c = baseDirection.compareTo(that.baseDirection);
if (c != 0) {
return c;
}
final double angle = angle(that);
if (angle < 0) return +1; // Really the opposite sign.
if (angle > 0) return -1; // Really the opposite sign.
return 0;
}
/**
* Tests this object for equality with the specified one.
* This method is used mostly for assertions.
*/
@Override
public boolean equals(final Object object) {
if (object instanceof DirectionAlongMeridian) {
final DirectionAlongMeridian that = (DirectionAlongMeridian) object;
return baseDirection.equals(that.baseDirection) &&
Numerics.equals(meridian, that.meridian);
}
return false;
}
/**
* Returns a hash code value, for consistency with {@link #equals}.
*/
@Override
public int hashCode() {
return Double.hashCode(meridian) + baseDirection.hashCode();
}
/**
* Returns a string representation of this direction, using a syntax matching the one used
* by EPSG. This string representation will be used for creating a new {@link AxisDirection}.
* The generated name should be identical to EPSG name, but we use the generated one anyway
* (rather than the one provided by EPSG) in order to make sure that we create a single
* {@link AxisDirection} for a given direction; we avoid potential differences like lower
* versus upper cases, amount of white space, <i>etc</i>.
*/
@Override
public String toString() {
String name = baseDirection.name();
final int length = name.length();
final StringBuilder buffer = new StringBuilder(length);
for (int i=0; i<length;) {
final int c = name.codePointAt(i);
buffer.appendCodePoint(i == 0 ? Character.toUpperCase(c) : Character.toLowerCase(c));
i += Character.charCount(c);
}
buffer.append(" along ");
final double md = Math.abs(meridian);
final int mi = (int) md;
if (md == mi) {
buffer.append(mi);
} else {
buffer.append(md);
}
buffer.append('°');
if (md != 0 && md != Longitude.MAX_VALUE) {
buffer.append(meridian < 0 ? 'W' : 'E');
}
name = buffer.toString();
assert EPSG.matcher(name).matches() : name;
return name;
}
/**
* Formats this object as a <cite>Well Known Text</cite> {@code Meridian[…]} element.
* This element contains the meridian value and the unit of measurement.
* The unit is currently fixed to degrees, but this may change in any future implementation.
*
* <div class="note"><b>Compatibility note:</b>
* {@code Meridian} is defined in the WKT 2 specification only.</div>
*
* @param formatter The formatter where to format the inner content of this WKT element.
* @return {@code "Meridian"}.
*
* @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#40">WKT 2 specification §7.5.4</a>
*/
@Override
protected String formatTo(final Formatter formatter) {
formatter.append(meridian);
formatter.append(Units.DEGREE);
return WKTKeywords.Meridian;
}
}