/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.wms;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.geotoolkit.client.CapabilitiesException;
import org.geotoolkit.client.Request;
import org.geotoolkit.storage.coverage.AbstractCoverageReference;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.coverage.io.GridCoverageWriter;
import org.geotoolkit.util.NamesExt;
import org.apache.sis.geometry.Envelope2D;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.internal.metadata.AxisDirections;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.apache.sis.referencing.CRS;
import org.geotoolkit.referencing.ReferencingUtilities;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.storage.DataStoreException;
import static org.apache.sis.util.ArgumentChecks.*;
import org.geotoolkit.util.StringUtilities;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.wms.xml.WMSVersion;
import org.opengis.util.GenericName;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.util.Utilities;
/**
* Coverage Reference for a WMS layer.
*
* @author Johann Sorel (Geomatys)
* @author Cédric Briançon (Geomatys)
* @module
*/
public class WMSCoverageReference extends AbstractCoverageReference{
protected static final Logger LOGGER = Logging.getLogger("org.geotoolkit.wms");
/**
* Configure the politic when the requested envelope is in CRS:84.
* Some servers are not strict on axis order or crs definitions.
* that's why we need this.
*/
public static enum CRS84Politic {
STRICT,
CONVERT_TO_EPSG4326
}
/**
* Configure the politic when the requested envelope is in EPSG:4326.
* Some servers are not strict on axis order or crs definitions.
* that's why we need this.
*/
public static enum EPSG4326Politic {
STRICT,
CONVERT_TO_CRS84
}
//TODO : we should use the envelope provided by the wms capabilities
private static final Envelope MAXEXTEND_ENV = new Envelope2D(
CommonCRS.WGS84.normalizedGeographic(), -180, -90, 360, 180);
/**
* The web map server to request.
*/
private final WebMapClient server;
/**
* Map for optional dimensions specified for the GetMap request.
*/
private final Map<String, String> dims = new HashMap<String, String>();
/**
* The layers to request.
*/
private GenericName[] layers;
/**
* The styles associated to the {@link #layers}.
*/
private String[] styles = new String[0];
/**
* Optional SLD file for the layer to request.
*/
private String sld = null;
/**
* Optional SLD version, if a SLD file have been given it is mandatory.
*/
private String sldVersion = null;
/**
* Optional SLD body directly in the request.
*/
private String sldBody = null;
/**
* Output format of the response.
*/
private String format = "image/png";
/**
* Transparence of the layer.
* WARNING: if we strictly respect the spec this value should be false.
*/
private Boolean transparent = true;
/**
* Output format of exceptions
*/
private String exceptionsFormat = null;
/**
* Use local reprojection of the image.
*/
private boolean useLocalReprojection = true;
/**
* If a daterange is defined in the envelope, move the temporal value
* on an existing value.
*/
private boolean matchCapabilitiesDates = false;
// hacks to fix some server not returning proper images
private CRS84Politic crs84Politic = CRS84Politic.STRICT;
private EPSG4326Politic epsg4326Politic = EPSG4326Politic.STRICT;
/**
* Expose a WMS as a normal coverage
*/
private WMSCoverageReader reader;
private Envelope env;
public WMSCoverageReference(final WebMapClient server, String ... layers) {
this(server,toNames(layers));
}
public WMSCoverageReference(final WebMapClient server, final GenericName ... names) {
super(server,names[0]);
this.server = server;
if(names == null || names.length == 0){
throw new IllegalArgumentException("No layer name defined");
}
this.layers = names;
}
private static GenericName[] toNames(String ... names){
if(names == null || names.length == 0){
return new GenericName[0];
}
final GenericName[] ns = new GenericName[names.length];
for(int i=0;i<names.length;i++){
final String str = names[i];
if(str != null && str.contains(",")){
throw new IllegalArgumentException("invalid layer, name must not contain ',' caractere : " + str);
}
ns[i] = NamesExt.valueOf(str);
}
return ns;
}
/**
* @return array of all layer names
*/
public GenericName[] getNames() throws DataStoreException{
return layers.clone();
}
@Override
public boolean isWritable() throws CoverageStoreException {
return false;
}
@Override
public int getImageIndex() {
return 0;
}
@Override
public GridCoverageWriter acquireWriter() throws CoverageStoreException {
throw new CoverageStoreException("WMS coverage are not writable.");
}
/**
* Sets the layer names to requests.
*
* @param names Array of layer names.
*/
public void setLayerNames(final String... names) {
this.layers = toNames(names);
}
/**
* Returns the layer names.
*/
public String[] getLayerNames() {
final String[] ns = new String[layers.length];
for(int i=0;i<layers.length;i++){
ns[i] = NamesExt.toExtendedForm(layers[i]);
}
return ns;
}
/**
* Returns a concatenated string of all layer names, separated by comma.
*/
public String getCombinedLayerNames() {
return StringUtilities.toCommaSeparatedValues((Object[])getLayerNames());
}
/**
* Sets the styles for the layers.
*
* @param styles Array of style names.
*/
public void setStyles(final String... styles) {
this.styles = styles;
}
/**
* Returns the style names.
*/
public String[] getStyles() {
return styles.clone();
}
/**
* Sets the sld value.
*
* @param sld A sld string.
*/
public void setSld(final String sld) {
this.sld = sld;
}
/**
* Gets the sld parameters. Can return {@code null}.
*/
public String getSld() {
return sld;
}
/**
* Sets the sldBody parameter.
*
* @param sldBody A sld body.
*/
public void setSldBody(final String sldBody) {
this.sldBody = sldBody;
}
/**
* Gets the sld body parameter of this request. Can return {@code null}.
*/
public String getSldBody() {
return sldBody;
}
/**
* Get the SLD specification version for SLD defines with SLD or SLD_BODY parameter
*
* @return the sldVersion
*/
public String getSldVersion() {
return sldVersion;
}
/**
* Set the SLD specification version for SLD defines with SLD or SLD_BODY parameter
*
* @param sldVersion
*/
public void setSldVersion(final String sldVersion) {
this.sldVersion = sldVersion;
}
/**
* Sets the format for the output response. By default sets to {@code image/png}
* if none.
*
* @param format The mime type of an output format.
*/
public void setFormat(final String format) {
ensureNonNull("format", format);
this.format = format;
}
/**
* Gets the format for the output response. By default {@code image/png}.
*/
public String getFormat() {
return format;
}
public Map<String, String> dimensions() {
return dims;
}
/**
* @return the exceptionsFormat
*/
public String getExceptionsFormat() {
return exceptionsFormat;
}
/**
* @param exceptionsFormat the exceptionsFormat to set
*/
public void setExceptionsFormat(final String exceptionsFormat) {
this.exceptionsFormat = exceptionsFormat;
}
/**
* @return the transparent
*/
public Boolean isTransparent() {
return transparent;
}
/**
* @param transparent the transparent to set
*/
public void setTransparent(final Boolean transparent) {
this.transparent = transparent;
}
public void setCrs84Politic(final CRS84Politic crs84Politic) {
ensureNonNull("CRS84 politic", crs84Politic);
this.crs84Politic = crs84Politic;
}
public CRS84Politic getCrs84Politic() {
return crs84Politic;
}
public void setEpsg4326Politic(final EPSG4326Politic epsg4326Politic) {
ensureNonNull("EPSG4326 politic", epsg4326Politic);
this.epsg4326Politic = epsg4326Politic;
}
public EPSG4326Politic getEpsg4326Politic() {
return epsg4326Politic;
}
/**
* Define if the map layer must rely on the geotoolkit reprojection capabilities
* if the distant server can not handle the canvas crs.
* The result image might not be pretty, but still better than no image.
* @param useLocalReprojection
*/
public void setUseLocalReprojection(final boolean useLocalReprojection) {
this.useLocalReprojection = useLocalReprojection;
}
public boolean isUseLocalReprojection() {
return useLocalReprojection;
}
/**
* Set to true if the time parameter must be adjusted to match the closest
* date provided in the layer getCapabilities.
* @param matchCapabilitiesDates
*/
public void setMatchCapabilitiesDates(final boolean matchCapabilitiesDates) {
this.matchCapabilitiesDates = matchCapabilitiesDates;
}
public boolean isMatchCapabilitiesDates() {
return matchCapabilitiesDates;
}
public Envelope getBounds() {
if(env == null){
try {
env = findEnvelope();
} catch (CapabilitiesException ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
}
if(env == null){
env = MAXEXTEND_ENV;
}
}
return env;
}
@Override
public synchronized GridCoverageReader acquireReader() throws CoverageStoreException{
if(reader == null){
reader = new WMSCoverageReader(this);
}
return reader;
}
/************************* Queries functions *****************************/
protected CoordinateReferenceSystem findOriginalCRS() throws FactoryException,CapabilitiesException {
return WMSUtilities.findOriginalCRS(server,getLayerNames()[0]);
}
protected boolean supportCRS(CoordinateReferenceSystem crs2D) throws FactoryException, CapabilitiesException {
return WMSUtilities.supportCRS(server,getLayerNames()[0],crs2D);
}
protected Envelope findEnvelope() throws CapabilitiesException {
return WMSUtilities.findEnvelope(server,getLayerNames()[0]);
}
protected Long findClosestDate(long l) throws CapabilitiesException {
return WMSUtilities.findClosestDate(server,getLayerNames()[0],(long)l);
}
/**
* Gives a {@linkplain GetMapRequest GetMap request} for the given envelope and
* output dimension. The default format will be {@code image/png} if the
* {@link #setFormat(java.lang.String)} has not been called.
*
* @param env A valid envlope to request.
* @param rect The dimension for the output response.
* @return A {@linkplain GetMapRequest get map request}.
* @throws MalformedURLException if the generated url is invalid.
* @throws TransformException if the tranformation between 2 CRS failed.
*/
public URL query(final Envelope env, final Dimension rect)
throws MalformedURLException, TransformException, FactoryException {
final GetMapRequest request = server.createGetMap();
prepareQuery(request, new GeneralEnvelope(env), rect, null);
return request.getURL();
}
/**
* Prepare parameters for a getMap query.
* The given parameters will be modified !
*
* @param request
* @param env
* @param dim
*/
public void prepareQuery(final GetMapRequest request, final GeneralEnvelope env,
final Dimension dim, final Point2D pickCoord) throws TransformException,
FactoryException{
//envelope before any modification
GeneralEnvelope beforeEnv = new GeneralEnvelope(env);
final CoordinateReferenceSystem crs = env.getCoordinateReferenceSystem();
CoordinateReferenceSystem crs2D = CRSUtilities.getCRS2D(crs);
GeneralEnvelope fakeEnv = new GeneralEnvelope(env);
//check if we must make the coverage reprojection ourself--------------
boolean supportCRS = false;
try {
supportCRS = supportCRS(crs2D);
} catch (CapabilitiesException ex) {
LOGGER.log(Level.WARNING, ex.toString(), ex);
}
if (isUseLocalReprojection() && !supportCRS) {
try {
crs2D = findOriginalCRS();
} catch (CapabilitiesException ex) {
//we tryed
crs2D = null;
}
if(crs2D == null){
//last chance use : EPSG:4326
crs2D = CommonCRS.WGS84.geographic();
}
if ((server.getVersion() == WMSVersion.v111) && (Utilities.equalsIgnoreMetadata(crs2D, CommonCRS.WGS84.normalizedGeographic()))) {
//in case we are asking for a WMS in 1.1.0 and CRS:84
//we must change the crs to 4326 but with CRS:84 coordinate
final GeneralEnvelope trsEnv = new GeneralEnvelope(ReferencingUtilities.transform2DCRS(env, CommonCRS.WGS84.normalizedGeographic()));
env.setEnvelope(trsEnv);
final CoordinateReferenceSystem fakeCrs = ReferencingUtilities.change2DComponent(crs, CommonCRS.WGS84.geographic());
trsEnv.setCoordinateReferenceSystem(fakeCrs);
fakeEnv.setEnvelope(trsEnv);
}else if (server.getVersion() == WMSVersion.v111) {
//in case we are asking for a WMS in 1.1.0 and a geographic crs
//we must set longitude coordinates first but preserve the crs
final CoordinateReferenceSystem lfcrs = ReferencingUtilities.setLongitudeFirst(crs2D);
final GeneralEnvelope trsEnv = new GeneralEnvelope(ReferencingUtilities.transform2DCRS(env, lfcrs));
env.setEnvelope(trsEnv);
trsEnv.setCoordinateReferenceSystem(ReferencingUtilities.change2DComponent(crs, crs2D));
fakeEnv.setEnvelope(trsEnv);
} else {
final GeneralEnvelope trsEnv = new GeneralEnvelope(ReferencingUtilities.transform2DCRS(env, crs2D));
env.setEnvelope(trsEnv);
fakeEnv.setEnvelope(trsEnv);
}
}else{
if ((server.getVersion() == WMSVersion.v111) && (Utilities.equalsIgnoreMetadata(crs2D, CommonCRS.WGS84.normalizedGeographic()))) {
//in case we are asking for a WMS in 1.1.0 and CRS:84
//we must change the crs to 4326 but with CRS:84 coordinate
final GeneralEnvelope trsEnv = new GeneralEnvelope(env);
final CoordinateReferenceSystem fakeCrs = ReferencingUtilities.change2DComponent(crs, CommonCRS.WGS84.geographic());
trsEnv.setCoordinateReferenceSystem(fakeCrs);
fakeEnv.setEnvelope(trsEnv);
} else if (server.getVersion() == WMSVersion.v111) {
//in case we are asking for a WMS in 1.1.0 and a geographic crs
//we must set longitude coordinates first but preserve the crs
final GeneralEnvelope trsEnv = new GeneralEnvelope(ReferencingUtilities.setLongitudeFirst(env));
trsEnv.setCoordinateReferenceSystem(crs);
fakeEnv.setEnvelope(trsEnv);
}
}
//WMS returns images with EAST-WEST axis first, so we ensure we modify the crs as expected
final Envelope longFirstEnvelope = ReferencingUtilities.setLongitudeFirst(env);
env.setEnvelope(longFirstEnvelope);
//Recalculate pick coordinate according to reverse transformation
if(pickCoord != null){
beforeEnv = (GeneralEnvelope) ReferencingUtilities.setLongitudeFirst(beforeEnv);
//calculate new coordinate in the reprojected query
final AffineTransform beforeTrs = ReferencingUtilities.toAffine(dim,beforeEnv);
final AffineTransform afterTrs = ReferencingUtilities.toAffine(dim,env);
try {
afterTrs.invert();
} catch (NoninvertibleTransformException ex) {
throw new TransformException("Failed to invert transform.",ex);
}
beforeTrs.transform(pickCoord, pickCoord);
final DirectPosition pos = new GeneralDirectPosition(env.getCoordinateReferenceSystem());
pos.setOrdinate(0, pickCoord.getX());
pos.setOrdinate(1, pickCoord.getY());
final MathTransform trs = CRS.findOperation(beforeEnv.getCoordinateReferenceSystem(), env.getCoordinateReferenceSystem(), null).getMathTransform();
trs.transform(pos, pos);
pickCoord.setLocation(pos.getOrdinate(0), pos.getOrdinate(1));
afterTrs.transform(pickCoord, pickCoord);
}
prepareGetMapRequest(request, fakeEnv, dim);
}
/**
* Prepare parameters for a GetMap request.
*
* @param request the GetMap request
* @param env A valid envelope to request.
* @param rect the output dimension
* @throws TransformException
*/
private void prepareGetMapRequest(final GetMapRequest request, Envelope env,
final Dimension rect) throws TransformException{
//check the politics, the distant wms server might not be strict on axis orders
// nor in it's crs definitions between CRS:84 and EPSG:4326
final CoordinateReferenceSystem crs2D = CRSUtilities.getCRS2D(env.getCoordinateReferenceSystem());
//we loose the vertical and temporale crs in the process, must be fixed
//check CRS84 politic---------------------------------------------------
if (crs84Politic != CRS84Politic.STRICT) {
if (Utilities.equalsIgnoreMetadata(crs2D, CommonCRS.WGS84.normalizedGeographic())) {
switch (crs84Politic) {
case CONVERT_TO_EPSG4326:
env = Envelopes.transform(env, crs2D);
env = new GeneralEnvelope(env);
((GeneralEnvelope) env).setCoordinateReferenceSystem(CommonCRS.WGS84.geographic());
break;
}
}
}
//check EPSG4326 politic------------------------------------------------
if (epsg4326Politic != EPSG4326Politic.STRICT) {
if (Utilities.equalsIgnoreMetadata(crs2D, CommonCRS.WGS84.geographic())) {
switch (epsg4326Politic) {
case CONVERT_TO_CRS84:
env = Envelopes.transform(env, crs2D);
env = new GeneralEnvelope(env);
((GeneralEnvelope) env).setCoordinateReferenceSystem(CommonCRS.WGS84.normalizedGeographic());
break;
}
}
}
if(matchCapabilitiesDates){
final CoordinateReferenceSystem crs = env.getCoordinateReferenceSystem();
final int index = dimensionColinearWith(crs.getCoordinateSystem(), CommonCRS.Temporal.JULIAN.crs().getCoordinateSystem().getAxis(0));
if(index >= 0){
//there is a temporal axis
final double median = env.getMedian(index);
Long closest = null;
try {
closest = findClosestDate((long)median);
} catch (CapabilitiesException ex) {
//at least we tryed
}
if(closest != null){
final GeneralEnvelope adjusted = new GeneralEnvelope(env);
adjusted.setRange(index, closest, closest);
env = adjusted;
LOGGER.log(Level.FINE, "adjusted : {0}", new Date(closest));
}
}
}
request.setEnvelope(env);
request.setDimension(rect);
request.setLayers(getLayerNames());
if (styles == null) {
request.setStyles("");
} else {
request.setStyles(styles);
}
request.setSld(sld);
request.setSldVersion(sldVersion);
request.setSldBody(sldBody);
request.setFormat(format);
request.setExceptions(exceptionsFormat);
request.setTransparent(transparent);
request.dimensions().putAll(dimensions());
}
@Override
public Image getLegend() throws DataStoreException {
final BufferedImage image;
try {
final Request getLegend =queryLegend(null, "image/png", null, null);
image = ImageIO.read(getLegend.getResponseStream());
} catch (IOException e) {
throw new DataStoreException(e);
}
return image;
}
/**
* Gives a {@linkplain GetLegendRequest GetLegendGraphic request} for
* the given dimension. The default format will be {@code image/png} if the
* {@link #setFormat(java.lang.String)} has not been called.
*
* @param rect the dimension of the image drawn
* @param format the format of the image drawn
* @param rule the SLD rule to draw
* @param scale the scale level of the SLD rule to draw
* @return A {@linkplain GetLegendRequest GetLegendGraphic request}.
* @throws MalformedURLException if the generated url is invalid.
*/
public Request queryLegend(final Dimension rect, final String format,
final String rule, final Double scale) throws MalformedURLException {
final GetLegendRequest request = server.createGetLegend();
prepareGetLegendRequest(request, rect, format, rule, scale);
return request;
}
/**
* Prepare parameters for a GetLegendGraphic request.
*
* @param request the GetLegend request
* @param rect the dimension of the image drawn
* @param format the format of the image drawn
* @param rule the SLD rule to draw
* @param scale the scale level of the SLD rule to draw
*/
protected void prepareGetLegendRequest(final GetLegendRequest request,
final Dimension rect, final String format, final String rule,
final Double scale) {
request.setDimension(rect);
request.setFormat(format);
request.setExceptions(exceptionsFormat);
request.setLayer(getLayerNames()[0]);
if (styles.length > 0) {
request.setStyle(styles[0]);
}
request.setSld(sld);
request.setSldBody(sldBody);
request.setRule(rule);
request.setScale(scale);
request.setSldVersion(sldVersion);
request.dimensions().putAll(dimensions());
}
/**
* Gives a {@linkplain GetFeatureInfoRequest GetFeatureInfo request} for the
* given envelope and output dimension. The default format will be
* {@code image/png} if the {@link #setFormat(java.lang.String)} has not
* been called.
*
* @param request the GetFeatureInfo request
* @param env the current envelope of the map
* @param rect the dimension of the map
* @param x X coordinate of the point
* @param y Y coordinate of the point
* @param queryLayers layers to query
* @param infoFormat output format of the GetFeatureInfo response
* @param featureCount max number of features to retrieve
* @return A {@linkplain GGetFeatureInfoRequest GetFeatureInfo request}.
* @throws TransformException
* @throws FactoryException
* @throws MalformedURLException if the generated url is invalid.
*/
public Request queryFeatureInfo(final Envelope env, final Dimension rect, int x,
int y, final String[] queryLayers, final String infoFormat,
final int featureCount) throws TransformException, FactoryException {
final GetFeatureInfoRequest request = server.createGetFeatureInfo();
prepareGetFeatureInfoRequest(request, env, rect, x, y, queryLayers,
infoFormat, featureCount);
return request;
}
/**
* Prepare parameters for a GetFeatureInfo request.
*
* @param request the GetFeatureInfo request
* @param env the current envelope of the map
* @param rect the dimension of the map
* @param x X coordinate of the point
* @param y Y coordinate of the point
* @param queryLayers layers to query
* @param infoFormat output format of the GetFeatureInfo response
* @param featureCount max number of features to retrieve
* @throws TransformException
* @throws FactoryException
* @throws MalformedURLException
*/
protected void prepareGetFeatureInfoRequest(final GetFeatureInfoRequest request,
final Envelope env, final Dimension rect, int x, int y,
final String[] queryLayers, final String infoFormat, final int featureCount)
throws TransformException, FactoryException {
request.setQueryLayers(queryLayers);
request.setInfoFormat(infoFormat);
request.setFeatureCount(featureCount);
final GeneralEnvelope cenv = new GeneralEnvelope(env);
final Dimension crect = new Dimension(rect);
final Point2D pickCoord = new Point2D.Double(x, y);
// Add the GetMap parameters
prepareQuery(request, cenv, crect, pickCoord);
request.setColumnIndex( (int)Math.round(pickCoord.getX()) );
request.setRawIndex( (int)Math.round(pickCoord.getY()) );
}
/**
* Returns the dimension within the coordinate system of the first occurrence of an axis
* colinear with the specified axis. If an axis with the same
* {@linkplain CoordinateSystemAxis#getDirection direction} or an
* {@linkplain AxisDirections#opposite opposite} direction than {@code axis}
* occurs in the coordinate system, then the dimension of the first such occurrence
* is returned. That is, the value <var>k</var> such that:
*
* {@preformat java
* absolute(cs.getAxis(k).getDirection()) == absolute(axis.getDirection())
* }
*
* is {@code true}. If no such axis occurs in this coordinate system,
* then {@code -1} is returned.
* <p>
* For example, {@code dimensionColinearWith(DefaultCoordinateSystemAxis.TIME)}
* returns the dimension number of time axis.
*
* @param cs The coordinate system to examine.
* @param axis The axis to look for.
* @return The dimension number of the specified axis, or {@code -1} if none.
*/
public static int dimensionColinearWith(final CoordinateSystem cs,
final CoordinateSystemAxis axis)
{
int candidate = -1;
final int dimension = cs.getDimension();
final AxisDirection direction = AxisDirections.absolute(axis.getDirection());
for (int i=0; i<dimension; i++) {
final CoordinateSystemAxis xi = cs.getAxis(i);
if (direction.equals(AxisDirections.absolute(xi.getDirection()))) {
candidate = i;
if (axis.equals(xi)) {
break;
}
}
}
return candidate;
}
}