From e1eea1cdb00e5cb9395b663dc43b38bf30995717 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 9 Mar 2025 14:53:45 +0100 Subject: [PATCH 1/7] Move to the root `pom.xml` the Maven configuration that was duplicated in modules. The intend is to avoid more duplication with the introduction of additional modules. The result should be identical, except the following: In the `maven-deploy-plugin`, the configuration was: * true in the `proj4j` module. * true in the `epsg` module. With this commit, the configuration become the following for the two modules: true true Everything else should be effectively identical. --- core/pom.xml | 170 ++------------------------------------------------- epsg/pom.xml | 168 ++------------------------------------------------ pom.xml | 164 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 175 insertions(+), 327 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 353f392..edfaba9 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -1,52 +1,18 @@ 4.0.0 + + org.locationtech.proj4j + proj4j-modules + 1.3.1-SNAPSHOT + + org.locationtech.proj4j proj4j - 1.3.1-SNAPSHOT bundle Proj4J - https://github.com/locationtech/proj4j Java port of the Proj.4 library for coordinate reprojection - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - https://github.com/locationtech/proj4j.git - scm:git:https://github.com/locationtech/proj4j.git - HEAD - - - - - echeipesh - Eugene Cheipesh - https://github.com/echeipesh - - - lossyrob - Rob Emanuele - https://github.com/lossyrob - - - pomadchin - Grigory Pomadchin - https://github.com/pomadchin - - - - - - Martin Davis - https://github.com/dr-jts - - - UTF-8 org.locationtech.proj4j @@ -71,53 +37,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 1.8 - 1.8 - true - UTF-8 - - - - true - maven-javadoc-plugin - 3.5.0 - - - attach-javadocs - - jar - - - true - false - false - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.1.0 - org.apache.felix maven-bundle-plugin @@ -135,84 +54,7 @@ true - - - - maven-deploy-plugin - 3.1.1 - - true - - - - default-deploy - deploy - - deploy - - - - - - - eclipse - - - repo.eclipse.org - Proj4J Repository - Releases - https://repo.eclipse.org/content/repositories/proj4j-releases/ - - - repo.eclipse.org - Proj4J Repository - Snapshots - https://repo.eclipse.org/content/repositories/proj4j-snapshots/ - - - - - central - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://oss.sonatype.org/ - false - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - - - - - diff --git a/epsg/pom.xml b/epsg/pom.xml index 184ebb3..3638880 100644 --- a/epsg/pom.xml +++ b/epsg/pom.xml @@ -1,12 +1,16 @@ 4.0.0 + + org.locationtech.proj4j + proj4j-modules + 1.3.1-SNAPSHOT + + org.locationtech.proj4j proj4j-epsg - 1.3.1-SNAPSHOT jar Proj4J EPSG - https://github.com/locationtech/proj4j Java port of the Proj.4 library for coordinate reprojection @@ -20,164 +24,4 @@ - - https://github.com/locationtech/proj4j.git - scm:git:https://github.com/locationtech/proj4j.git - HEAD - - - - - echeipesh - Eugene Cheipesh - https://github.com/echeipesh - - - lossyrob - Rob Emanuele - https://github.com/lossyrob - - - pomadchin - Grigory Pomadchin - https://github.com/pomadchin - - - - - - Martin Davis - https://github.com/dr-jts - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 1.8 - 1.8 - true - UTF-8 - - - - true - maven-javadoc-plugin - 3.5.0 - - - attach-javadocs - - jar - - - true - false - false - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.1.0 - - - - - maven-deploy-plugin - 3.1.1 - - true - - - - default-deploy - deploy - - deploy - - - - - - - - - - eclipse - - - repo.eclipse.org - Proj4J Repository - Releases - https://repo.eclipse.org/content/repositories/proj4j-releases/ - - - repo.eclipse.org - Proj4J Repository - Snapshots - https://repo.eclipse.org/content/repositories/proj4j-snapshots/ - - - - - central - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://oss.sonatype.org/ - false - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - - - - - diff --git a/pom.xml b/pom.xml index 646adb4..dcb63b4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ 1.3.1-SNAPSHOT pom @@ -21,6 +21,168 @@ + + https://github.com/locationtech/proj4j.git + scm:git:https://github.com/locationtech/proj4j.git + HEAD + + + + + echeipesh + Eugene Cheipesh + https://github.com/echeipesh + + + lossyrob + Rob Emanuele + https://github.com/lossyrob + + + pomadchin + Grigory Pomadchin + https://github.com/pomadchin + + + + + + Martin Davis + https://github.com/dr-jts + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 1.8 + 1.8 + true + UTF-8 + + + + true + maven-javadoc-plugin + 3.5.0 + + + attach-javadocs + + jar + + + true + false + false + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.0 + + + + + maven-deploy-plugin + 3.1.1 + + true + true + + + + default-deploy + deploy + + deploy + + + + + + + + + + eclipse + + + repo.eclipse.org + Proj4J Repository - Releases + https://repo.eclipse.org/content/repositories/proj4j-releases/ + + + repo.eclipse.org + Proj4J Repository - Snapshots + https://repo.eclipse.org/content/repositories/proj4j-snapshots/ + + + + + central + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.13 + true + + ossrh + https://oss.sonatype.org/ + false + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + + + + + + core epsg From 9b89243a2fdf56c90173198825976169726f0386 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Sun, 9 Mar 2025 15:06:12 +0100 Subject: [PATCH 2/7] Move the declaration of JUnit dependency version in the root `pom.xml`, in order to manage the dependency version in a single place for all modules. --- core/pom.xml | 1 - pom.xml | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/pom.xml b/core/pom.xml index edfaba9..babb4bf 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -30,7 +30,6 @@ junit junit - 4.13.2 test diff --git a/pom.xml b/pom.xml index dcb63b4..c9f4f1e 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,17 @@ + + + + junit + junit + 4.13.2 + test + + + + From f64c5cc6fc930cbec7dcae73212c5ff7fb2d1256 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Mon, 10 Mar 2025 01:15:44 +0100 Subject: [PATCH 3/7] Add GeoAPI wrappers for GeographicCRS and its dependencies. --- geoapi/pom.xml | 72 +++++++ .../proj4j/geoapi/AbstractCRS.java | 180 +++++++++++++++++ .../org/locationtech/proj4j/geoapi/Alias.java | 125 ++++++++++++ .../org/locationtech/proj4j/geoapi/Axis.java | 188 ++++++++++++++++++ .../proj4j/geoapi/DatumWrapper.java | 140 +++++++++++++ .../proj4j/geoapi/EllipsoidWrapper.java | 137 +++++++++++++ .../proj4j/geoapi/GeographicCRSWrapper.java | 50 +++++ .../proj4j/geoapi/LocalizedString.java | 100 ++++++++++ .../proj4j/geoapi/PrimeMeridianWrapper.java | 104 ++++++++++ .../org/locationtech/proj4j/geoapi/Units.java | 101 ++++++++++ .../locationtech/proj4j/geoapi/Wrapper.java | 173 ++++++++++++++++ .../locationtech/proj4j/geoapi/Wrappers.java | 93 +++++++++ .../proj4j/geoapi/package-info.java | 35 ++++ .../proj4j/geoapi/WrappersTest.java | 104 ++++++++++ pom.xml | 1 + 15 files changed, 1603 insertions(+) create mode 100644 geoapi/pom.xml create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/Alias.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/Axis.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/LocalizedString.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java create mode 100644 geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java diff --git a/geoapi/pom.xml b/geoapi/pom.xml new file mode 100644 index 0000000..80731e4 --- /dev/null +++ b/geoapi/pom.xml @@ -0,0 +1,72 @@ + + 4.0.0 + + + org.locationtech.proj4j + proj4j-modules + 1.3.1-SNAPSHOT + + + org.locationtech.proj4j + proj4j-geoapi + jar + GeoAPI wrappers + GeoAPI wrappers for viewing PROJ4J as a GeoAPI implementation. + + + + desruisseaux + Martin Desruisseaux + https://github.com/desruisseaux + + + + + + org.opengis + geoapi + 3.0.2 + + + org.locationtech.proj4j + proj4j + ${project.version} + + + org.locationtech.proj4j + proj4j-epsg + ${project.version} + test + + + tech.uom + seshat + 1.3 + test + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + false + all + true + true + + https://www.geoapi.org/3.0/javadoc/ + + + + + + + diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java new file mode 100644 index 0000000..9ef0a35 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java @@ -0,0 +1,180 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Arrays; +import org.locationtech.proj4j.ProjCoordinate; +import org.locationtech.proj4j.datum.AxisOrder; +import org.locationtech.proj4j.proj.Projection; +import org.opengis.referencing.crs.SingleCRS; +import org.opengis.referencing.cs.CoordinateSystem; +import org.opengis.referencing.cs.CoordinateSystemAxis; +import org.opengis.referencing.datum.GeodeticDatum; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * The CRS is assumed two-dimensional. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +abstract class AbstractCRS extends Wrapper implements SingleCRS, CoordinateSystem, Serializable { + /** + * The number of dimensions of the CRS. + */ + private static final int BIDIMENSIONAL = 2; + + /** + * The wrapped PROJ4 implementation. + */ + private final org.locationtech.proj4j.CoordinateReferenceSystem impl; + + /** + * The coordinate system axes, computed and cached when first requested. + * This is refreshed every time that {@link #getCoordinateSystem()} is invoked, + * for compliance with the documentation saying that this object is a view. + */ + @SuppressWarnings("VolatileArrayField") // Because array elements will not change. + private volatile transient Axis[] axes; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + AbstractCRS(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static AbstractCRS wrap(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { + if (impl != null) { + final Projection proj = impl.getProjection(); + if (proj == null || proj.isGeographic()) { + return new GeographicCRSWrapper(impl); + } else { + // TODO + } + } + return null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + final Object implementation() { + return impl; + } + + /** + * {@return the CRS name}. + */ + @Override + public final String getCode() { + return impl.getName(); + } + + /** + * {@return the PROJ4J datum wrapped behind the GeoAPI interface}. + */ + @Override + public final GeodeticDatum getDatum() { + return DatumWrapper.wrap(impl); + } + + /** + * {@return the coordinate system, which is implemented by the same class for convenience}. + */ + @Override + public CoordinateSystem getCoordinateSystem() { + clearAxisCache(); + return this; + } + + /** + * {@return the number of dimensions, which is fixed to 2}. + */ + @Override + public final int getDimension() { + return BIDIMENSIONAL; + } + + /** + * Returns {@link Axis#GEOGRAPHIC} and {@link Axis#PROJECTED} arrays, + * depending on whether this CRS is geographic or projected. + * The returned array is not cloned, the caller shall not modify it. + */ + abstract Axis[] axesForAllDirections(); + + /** + * Clears the cache of axes. This method should be invoked by {@link #getCoordinateSystem()} + * for compliance with the documentation saying that change in the wrapped object are reflected + * in the view. + */ + final void clearAxisCache() { + axes = null; + } + + /** + * Returns the axis in the given dimension. + * + * @param dimension the axis index, from 0 to 2 inclusive. + * @return axis in the specified dimension. + * @throws IndexOutOfBoundsException if the given axis index is out of bounds. + */ + @Override + public final CoordinateSystemAxis getAxis(int dimension) { + @SuppressWarnings("LocalVariableHidesMemberVariable") + Axis[] axes = this.axes; + if (axes == null) { + final Axis[] axesForAllDirections = axesForAllDirections(); + axes = Arrays.copyOfRange(axesForAllDirections, Axis.INDEX_OF_EAST, axesForAllDirections.length); + final Projection proj = impl.getProjection(); + if (proj != null) { + final AxisOrder order = proj.getAxisOrder(); + if (order != null) { + ProjCoordinate coord = new ProjCoordinate(1, 2, 3); + order.fromENU(coord); + for (int i=0; i wrap(final String name) { + return (name != null) ? Collections.singletonList(new Alias(name)) : Collections.emptyList(); + } + + @Override + public NameSpace scope() { + return null; + } + + @Override + public int depth() { + return 1; + } + + @Override + public List getParsedNames() { + return Collections.singletonList(this); + } + + @Override + public LocalName head() { + return this; + } + + @Override + public LocalName tip() { + return this; + } + + @Override + public GenericName toFullyQualifiedName() { + return this; + } + + @Override + public ScopedName push(GenericName scope) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public InternationalString toInternationalString() { + return LocalizedString.wrap(name); + } + + @Override + public String toString() { + return name; + } + + @Override + public int compareTo(GenericName o) { + int c = name.compareTo(o.head().toString()); + if (c == 0) { + c = depth() - o.depth(); + } + return c; + } + + @Override + public boolean equals(Object o) { + return (o instanceof Alias) && name.equals(((Alias) o).name); + } + + @Override + public int hashCode() { + return name.hashCode() ^ getClass().hashCode(); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Axis.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Axis.java new file mode 100644 index 0000000..8224194 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Axis.java @@ -0,0 +1,188 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import javax.measure.Unit; +import org.opengis.referencing.cs.AxisDirection; +import org.opengis.referencing.cs.CoordinateSystemAxis; +import org.opengis.referencing.cs.RangeMeaning; + + +/** + * A coordinate system axis. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class Axis extends Wrapper implements CoordinateSystemAxis, Serializable { + /** + * The axes for a geographic or projected CRS. + * Order is (down, west, south, null, east, north, up). + * Each axis shall be in the array at the index equal to {@link #direction} + 3. + */ + static final Axis[] GEOGRAPHIC, PROJECTED; + static { + GEOGRAPHIC = new Axis[] { + new Axis("Ellipsoidal depth", "d", (byte) -3, false, 1), + new Axis("Geodetic latitude", "lat", (byte) -2, true, 1), + new Axis("Geodetic longitude", "lon", (byte) -1, true, 1), + null, + new Axis("Geodetic longitude", "lon", (byte) 1, true, 1), + new Axis("Geodetic latitude", "lat", (byte) 2, true, 1), + new Axis("Ellipsoidal height", "h", (byte) 3, false, 1) + }; + PROJECTED = new Axis[] { + GEOGRAPHIC[0], + new Axis("Southing", "S", (byte) -2, false, 1), + new Axis("Westing", "W", (byte) -1, false, 1), + null, + new Axis("Easting", "E", (byte) 1, false, 1), + new Axis("Northing", "N", (byte) 2, false, 1), + GEOGRAPHIC[6] + }; + } + + /** + * The axis directions in the order declared in the {@link #GEOGRAPHIC} and {@link #PROJECTED} arrays. + */ + private static final AxisDirection[] DIRECTIONS = { + AxisDirection.DOWN, + AxisDirection.SOUTH, + AxisDirection.WEST, + null, + AxisDirection.EAST, + AxisDirection.NORTH, + AxisDirection.UP + }; + + /** + * Index of the axis having the east direction in {@link #GEOGRAPHIC} and {@link #PROJECTED} arrays. + */ + static final int INDEX_OF_EAST = 4; + + /** + * The coordinate system axis name. + */ + private final String name; + + /** + * The coordinate system axis abbreviation. + */ + private final String abbreviation; + + /** + * The axis direction: 1=east, 2=north, 3=up. + * The value may be negative for the opposite direction. + */ + private final byte direction; + + /** + * Whether the unit of measurement is degrees or metres. + */ + private final boolean angular; + + /** + * The scale factor to apply on unit of measurement. + * For angular units, the base unit is degree, not radian. + */ + private final double unitScale; + + /** + * Unit of measurement, cached when first requested. + */ + private transient Unit unit; + + /** + * Creates a new axis. + * + * @param name the coordinate system axis name + * @param abbreviation the coordinate system axis abbreviation + * @param north whether the axis is oriented toward north or east. + * @param angular whether the unit of measurement is degrees or metres. + * @param unitScale the scale factor to apply on unit of measurement. + */ + private Axis(final String name, final String abbreviation, final byte direction, final boolean angular, final double unitScale) { + this.name = name; + this.abbreviation = abbreviation; + this.direction = direction; + this.angular = angular; + this.unitScale = unitScale; + } + + /** + * Returns the same axis but with a unit of measurement multiplied by the given scale. + */ + final Axis withUnit(final double scale) { + if (scale == unitScale) { + return this; + } + return new Axis(name, abbreviation, direction, angular, scale); + } + + /** + * {@return an arbitrary value suitable for string representation}. + */ + @Override + Object implementation() { + return name; + } + + /** + * {@return the axis name}. + */ + @Override + public String getCode() { + return name; + } + + /** + * {@return the axis abbreviation}. + */ + @Override + public String getAbbreviation() { + return abbreviation; + } + + @Override + public AxisDirection getDirection() { + return DIRECTIONS[direction + 3]; + } + + @Override + public double getMinimumValue() { + return Double.NEGATIVE_INFINITY; + } + + @Override + public double getMaximumValue() { + return Double.POSITIVE_INFINITY; + } + + @Override + public RangeMeaning getRangeMeaning() { + return angular && (Math.abs(direction) == 1) ? RangeMeaning.WRAPAROUND : RangeMeaning.EXACT; + } + + @Override + public Unit getUnit() { + if (unit == null) { + final Units units = Units.getInstance(); + unit = (angular ? units.degree : units.metre).multiply(unitScale); + } + return unit; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java new file mode 100644 index 0000000..32b7fa1 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java @@ -0,0 +1,140 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Date; +import org.locationtech.proj4j.datum.Datum; +import org.opengis.referencing.datum.Ellipsoid; +import org.opengis.referencing.datum.GeodeticDatum; +import org.opengis.referencing.datum.PrimeMeridian; +import org.opengis.util.GenericName; +import org.opengis.util.InternationalString; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class DatumWrapper extends Wrapper implements GeodeticDatum, Serializable { + /** + * The wrapped PROJ4 implementation. + */ + private final org.locationtech.proj4j.datum.Datum impl; + + /** + * The prime meridian, or {@code null} for Greenwich + */ + private final org.locationtech.proj4j.datum.PrimeMeridian pm; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + private DatumWrapper(final org.locationtech.proj4j.datum.Datum impl, + final org.locationtech.proj4j.datum.PrimeMeridian pm) + { + this.impl = impl; + this.pm = pm; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static DatumWrapper wrap(final org.locationtech.proj4j.datum.Datum impl) { + return (impl != null) ? new DatumWrapper(impl, null) : null; + } + + /** + * Wraps the given implementation. + * + * @param crs the CRS to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static DatumWrapper wrap(final org.locationtech.proj4j.CoordinateReferenceSystem crs) { + if (crs != null) { + Datum impl = crs.getDatum(); + if (impl != null) { + return new DatumWrapper(impl, PrimeMeridianWrapper.ifNonGreenwich(crs.getProjection())); + } + } + return null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + Object implementation() { + return impl; + } + + /** + * {@return the long name if available, or the short name otherwise}. + * In the EPSG database, the primary name is usually the long name. + */ + @Override + public String getCode() { + String name = impl.getName(); + if (name == null) { + name = impl.getCode(); + } + return name; + } + + /** + * {@return other names of this object}. + * In the EPSG database, this is usually the short name (the abbreviation). + */ + @Override + public Collection getAlias() { + if (impl.getName() != null) { + return Alias.wrap(impl.getCode()); + } + return super.getAlias(); + } + + /** + * {@return the PROJ4J ellipsoid wrapped behind the GeoAPI interface}. + */ + @Override + public Ellipsoid getEllipsoid() { + return EllipsoidWrapper.wrap(impl.getEllipsoid()); + } + + /** + * {@return the hard-coded Greenwich prime meridian}. + */ + @Override + public PrimeMeridian getPrimeMeridian() { + return PrimeMeridianWrapper.wrap(pm); + } + + @Override + public InternationalString getAnchorPoint() { + return null; + } + + @Override + public Date getRealizationEpoch() { + return null; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java new file mode 100644 index 0000000..c61efab --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java @@ -0,0 +1,137 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Collection; +import javax.measure.Unit; +import javax.measure.quantity.Length; +import org.opengis.referencing.datum.Ellipsoid; +import org.opengis.util.GenericName; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class EllipsoidWrapper extends Wrapper implements Ellipsoid, Serializable { + /** + * The wrapped PROJ4 implementation. + */ + private final org.locationtech.proj4j.datum.Ellipsoid impl; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + private EllipsoidWrapper(final org.locationtech.proj4j.datum.Ellipsoid impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static EllipsoidWrapper wrap(final org.locationtech.proj4j.datum.Ellipsoid impl) { + return (impl != null) ? new EllipsoidWrapper(impl) : null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + Object implementation() { + return impl; + } + + /** + * {@return the long name if available, or the short name otherwise}. + * In the EPSG database, the primary name is usually the long name. + */ + @Override + public String getCode() { + String name = impl.getName(); + if (name == null) { + name = impl.getShortName(); + } + return name; + } + + /** + * {@return other names of this object}. + * In the EPSG database, this is usually the short name (the abbreviation). + */ + @Override + public Collection getAlias() { + if (impl.getName() != null) { + return Alias.wrap(impl.getShortName()); + } + return super.getAlias(); + } + + /** + * @return the axis unit of measurement, which is assumed to be metres. + */ + @Override + public Unit getAxisUnit() { + return Units.getInstance().metre; + } + + /** + * {@return the equator radius of the PROJ4J implementation}. + */ + @Override + public double getSemiMajorAxis() { + return impl.getA(); + } + + /** + * {@return the pole radius of the PROJ4J implementation}. + */ + @Override + public double getSemiMinorAxis() { + return impl.getB(); + } + + /** + * {@return computes the inverse flatening from the equator and pole radius}. + */ + @Override + public double getInverseFlattening() { + final double a = impl.getA(); + return a / (a - impl.getB()); + } + + /** + * {@return false since the inverse flatteing is computed}. + */ + @Override + public boolean isIvfDefinitive() { + return false; + } + + /** + * @return whether the equator and pole radius are equal. + * Strict equality is okay because those values are set explicitly. + */ + @Override + public boolean isSphere() { + return impl.getA() == impl.getB(); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java new file mode 100644 index 0000000..e08f937 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.cs.EllipsoidalCS; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * The CRS is assumed two-dimensional. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class GeographicCRSWrapper extends AbstractCRS implements EllipsoidalCS, GeographicCRS { + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + GeographicCRSWrapper(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { + super(impl); + } + + /** + * {@return the coordinate system, which is implemented by the same class for convenience}. + */ + @Override + public EllipsoidalCS getCoordinateSystem() { + clearAxisCache(); + return this; + } + + @Override + final Axis[] axesForAllDirections() { + return Axis.GEOGRAPHIC; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/LocalizedString.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/LocalizedString.java new file mode 100644 index 0000000..8c10476 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/LocalizedString.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Locale; +import org.opengis.util.InternationalString; + + +/** + * A string in a specific locale. + * In the current version, the locale is unspecified. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class LocalizedString implements InternationalString, Serializable { + /** + * The "not known" value. ISO 19111 requires that we return this string + * if the scope of a datum or coordinate operation is unknown. + */ + static final LocalizedString UNKNOWN = new LocalizedString("not known"); + + /** + * The localized text. + */ + private final String text; + + /** + * Creates a new international string. + * + * @param text the localized text + */ + private LocalizedString(final String text) { + this.text = text; + } + + /** + * Returns the given text as an international string. + * + * @param text the localized text, or {@code null} + * @return the international string, or {@code null} if the given text was null + */ + static LocalizedString wrap(final String text) { + return (text != null) ? new LocalizedString(text) : null; + } + + @Override + public String toString() { + return text; + } + + @Override + public String toString(Locale locale) { + return text; + } + + @Override + public int length() { + return text.length(); + } + + @Override + public char charAt(int index) { + return text.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return text.subSequence(start, end); + } + + @Override + public int compareTo(InternationalString o) { + return text.compareTo(o.toString()); + } + + @Override + public boolean equals(Object o) { + return (o instanceof LocalizedString) && text.equals(((LocalizedString) o).text); + } + + @Override + public int hashCode() { + return text.hashCode() ^ getClass().hashCode(); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java new file mode 100644 index 0000000..a9a5e48 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import javax.measure.Unit; +import javax.measure.quantity.Angle; +import org.locationtech.proj4j.ProjCoordinate; +import org.locationtech.proj4j.proj.Projection; +import org.opengis.referencing.datum.PrimeMeridian; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class PrimeMeridianWrapper extends Wrapper implements PrimeMeridian, Serializable { + /** + * The Greenwich prime meridian. + */ + private static final PrimeMeridianWrapper GREENWICH = new PrimeMeridianWrapper( + org.locationtech.proj4j.datum.PrimeMeridian.forName("greenwich")); + + /** + * The wrapped PROJ4 implementation. + */ + private final org.locationtech.proj4j.datum.PrimeMeridian impl; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + private PrimeMeridianWrapper(final org.locationtech.proj4j.datum.PrimeMeridian impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or Greenwich if the given implementation was null + */ + static PrimeMeridianWrapper wrap(final org.locationtech.proj4j.datum.PrimeMeridian impl) { + return (impl != null) ? new PrimeMeridianWrapper(impl) : GREENWICH; + } + + /** + * Returns the prime meridian of the given projection if different from Greenwich. + * + * @param proj the projection from which to get the prime meridian, or {@code null} + * @return the prime meridian if different than Greenwich, or {@code null} otherwise. + */ + static org.locationtech.proj4j.datum.PrimeMeridian ifNonGreenwich(final Projection proj) { + if (proj != null) { + org.locationtech.proj4j.datum.PrimeMeridian impl = proj.getPrimeMeridian(); + if (impl != null && !GREENWICH.impl.equals(impl)) { + return impl; + } + } + return null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + Object implementation() { + return impl; + } + + /** + * {@return the name}. + */ + @Override + public String getCode() { + return impl.getName(); + } + + @Override + public double getGreenwichLongitude() { + ProjCoordinate coord = new ProjCoordinate(); + impl.toGreenwich(coord); + return coord.x; + } + + @Override + public Unit getAngularUnit() { + return Units.getInstance().degree; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java new file mode 100644 index 0000000..bfd16f0 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java @@ -0,0 +1,101 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import javax.measure.Unit; +import javax.measure.quantity.Angle; +import javax.measure.quantity.Length; +import javax.measure.quantity.Dimensionless; +import javax.measure.spi.ServiceProvider; +import javax.measure.spi.SystemOfUnits; + + +/** + * Predefined constants for the units of measurement. + * The actual JSR-363 implementation is left at user's choice. + * + * @author Martin Desruisseaux (Geomatys) + */ +final class Units { + /** + * The default instance, created when first needed. + * + * @see #getInstance() + */ + private static Units instance; + + /** + * The implementation-dependent system of units for creating base units. + */ + public final SystemOfUnits system; + + /** + * Linear unit. + */ + public final Unit metre; + + /** + * Angular unit. + */ + public final Unit degree, radian; + + /** + * Scale unit. + */ + public final Unit one, ppm; + + /** + * Creates a new set of units which will use the given system of units. + * + * @param system the system of units to use for creating base units + */ + private Units(final SystemOfUnits system) { + this.system = system; + metre = system.getUnit(Length.class); + radian = system.getUnit(Angle.class); + one = getDimensionless(system); + degree = radian.multiply(StrictMath.PI / 180); + ppm = one .divide(1000000); + } + + /** + * {@return the default units factory}. This factory uses the unit service provider which is + * {@linkplain ServiceProvider#current() current} at the time of the first invocation of this method. + */ + public static synchronized Units getInstance() { + if (instance == null) { + instance = new Units(ServiceProvider.current().getSystemOfUnitsService().getSystemOfUnits()); + } + return instance; + } + + /** + * Returns the dimensionless unit. This is a workaround for what seems to be a bug + * in the reference implementation 1.0.1 of unit API. + * + * @param system the system of units from which to get the dimensionless unit. + * @return the dimensionless unit. + */ + private static Unit getDimensionless(final SystemOfUnits system) { + Unit unit = system.getUnit(Dimensionless.class); + if (unit == null) try { + unit = ((Unit) Class.forName("tec.units.ri.AbstractUnit").getField("ONE").get(null)).asType(Dimensionless.class); + } catch (ReflectiveOperationException | ClassCastException e) { + throw new IllegalArgumentException("Can not create a dimensionless unit from the given provider."); + } + return unit; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java new file mode 100644 index 0000000..36696cf --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java @@ -0,0 +1,173 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import org.opengis.metadata.citation.Citation; +import org.opengis.metadata.extent.Extent; +import org.opengis.referencing.IdentifiedObject; +import org.opengis.referencing.ReferenceIdentifier; +import org.opengis.util.GenericName; +import org.opengis.util.InternationalString; + + +/** + * Base class for wrappers around PROJ4J implementations. + * Subclasses should return the object name in the {@link #getCode()} method. + * + * @author Martin Desruisseaux (Geomatys) + */ +abstract class Wrapper implements IdentifiedObject, ReferenceIdentifier { + /** + * Creates a new wrapper. + */ + Wrapper() { + } + + /** + * {@return the wrapped implementation}. + */ + abstract Object implementation(); + + /** + * {@return the authority that defines this object}. + * The default implementation assumes that there is none. + */ + @Override + public Citation getAuthority() { + return null; + } + + /** + * {@return a short name of the authority used as a code space}. + * The default implementation returns "PROJ4J" on the assumption that the names are specific to PROJ4J. + * This is not completely true since those names are often derived from EPSG, but we don't really have + * a guarantee that they are exact or that PROJ4J didn't added their own definitions. + */ + @Override + public String getCodeSpace() { + return "PROJ4J"; + } + + /** + * {@return the version of the defined object}. + * The default implementation assumes that there is none. + */ + @Override + public String getVersion() { + return null; + } + + /** + * {@return the primary object name}. + * In the EPSG database, this is usually the long name. + */ + @Override + public abstract String getCode(); + + /** + * {@return the primary object name}. This method returns {@code this}, + * with the expectation that users will follow with {@link #getCode()}. + * Subclasses shall return the actual object name in {@code getCode()}. + */ + @Override + public final ReferenceIdentifier getName() { + return this; + } + + /** + * {@return other names of this object}. + * In the EPSG database, this is usually the short name. + * The default implementation assumes that there is none. + */ + @Override + public Collection getAlias() { + return Collections.emptyList(); + } + + /** + * {@return all identifiers (usually EPSG codes) of this object}. + * The default implementation assumes that there is none. + */ + @Override + public Set getIdentifiers() { + return Collections.emptySet(); + } + + /** + * {@return the scope of usage of this object}. + * If unknown, ISO 19111 requires that we return "not known". + */ + public InternationalString getScope() { + return LocalizedString.UNKNOWN; + } + + /** + * {@return the domain of validity of this object}. + * The default implementation assumes that there is none. + */ + public Extent getDomainOfValidity() { + return null; + } + + /** + * {@return optional remarks about this object}. + * The default implementation assumes that there is none. + */ + @Override + public InternationalString getRemarks() { + return null; + } + + /** + * {@return a WKT representation of this object}. + * The default implementation assumes that there is none. + */ + @Override + public String toWKT() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported."); + } + + /** + * {@return the string representation of the wrapped PROJ4J object}. + */ + @Override + public final String toString() { + return implementation().toString(); + } + + /** + * {@return a hash code value for this wrapper}. + */ + @Override + public final int hashCode() { + return implementation().hashCode() ^ getClass().hashCode(); + } + + /** + * Compares this wrapper with the given object for equality. This method returns {@code true} + * if the two objects are wrappers of the same class wrapping equal PROJ4 implementations. + */ + @Override + public final boolean equals(final Object other) { + if (other != null && other.getClass() == getClass()) { + return implementation().equals(((Wrapper) other).implementation()); + } + return false; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java new file mode 100644 index 0000000..3f8e788 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.crs.ProjectedCRS; +import org.opengis.referencing.crs.SingleCRS; +import org.opengis.referencing.datum.Ellipsoid; +import org.opengis.referencing.datum.GeodeticDatum; +import org.opengis.referencing.datum.PrimeMeridian; + + +/** + * Views of PROJ4J implementation classes as GeoAPI objects. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class Wrappers { + /** + * Do not allow instantiation of this class. + */ + private Wrappers() { + } + + /** + * Wraps the given PROJ4J CRS behind the equivalent GeoAPI interface. + * The returned object is a view: if any {@code impl} value is changed after this method call, + * those changes will be reflected immediately in the returned view. Note that CRS objects + * should be immutable. Therefore, it is recommended to not apply any change on {@code impl}. + * + *

There is one exception to above paragraph: this method determines immediately whether the given + * CRS is a {@link GeographicCRS} or {@link ProjectedCRS}. That type of the view cannot + * be changed after construction.

+ * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static SingleCRS geoapi(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { + return AbstractCRS.wrap(impl); + } + + /** + * Wraps the given PROJ4J datum behind the equivalent GeoAPI interface. + * The returned object is a view: if any {@code impl} value is changed after this method call, + * those changes will be reflected immediately in the returned view. Note that CRS objects + * should be immutable. Therefore, it is recommended to not apply any change on {@code impl}. + * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static GeodeticDatum geoapi(final org.locationtech.proj4j.datum.Datum impl) { + return DatumWrapper.wrap(impl); + } + + /** + * Wraps the given PROJ4J ellipsoid behind the equivalent GeoAPI interface. + * The returned object is a view: if any {@code impl} value is changed after this method call, + * those changes will be reflected immediately in the returned view. Note that CRS objects + * should be immutable. Therefore, it is recommended to not apply any change on {@code impl}. + * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static Ellipsoid geoapi(final org.locationtech.proj4j.datum.Ellipsoid impl) { + return EllipsoidWrapper.wrap(impl); + } + + /** + * Wraps the given PROJ4J ellipsoid behind the equivalent GeoAPI interface. + * The returned object is a view: if any {@code impl} value is changed after this method call, + * those changes will be reflected immediately in the returned view. Note that CRS objects + * should be immutable. Therefore, it is recommended to not apply any change on {@code impl}. + * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static PrimeMeridian geoapi(final org.locationtech.proj4j.datum.PrimeMeridian impl) { + return PrimeMeridianWrapper.wrap(impl); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java new file mode 100644 index 0000000..e8fc70d --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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. + */ + +/** + * Wraps the PROJ4J classes behind the equivalent GeoAPI interfaces. + * This module provides a public class, {@link org.locationtech.proj4j.geoapi.Wrappers}, + * with overloaded {@code geoapi(…)} methods. Those methods expected a PROJ4J object in + * argument and returns a view of that object as a GeoAPI type. + * + *

Mutability

+ * No information is copied. All methods of the views delegate their work to the PROJ4J implementation. + * Consequently, since PROJ4J objects are mutable, changes to the wrapped PROJ4J object are immediately + * reflected in the view. However, it is not recommended to change a wrapped PROJ4J object as CRS should + * be immutable. + * + *

There is one exception to the above paragraph: whether an object is a geographic or projected CRS. + * Because the type of a Java object cannot change dynamically, whether a CRS is geographic or projected + * is determined at {@code geoapi(CoordinateReferenceSystem)} invocation time.

+ * + * @author Martin Desruisseaux (Geomatys) + */ +package org.locationtech.proj4j.geoapi; diff --git a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java new file mode 100644 index 0000000..513dfaf --- /dev/null +++ b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import javax.measure.Unit; +import javax.measure.quantity.Angle; +import org.junit.Test; +import org.locationtech.proj4j.CRSFactory; +import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.datum.GeodeticDatum; +import org.opengis.referencing.datum.PrimeMeridian; +import org.opengis.referencing.datum.Ellipsoid; +import org.opengis.referencing.cs.EllipsoidalCS; +import org.opengis.referencing.cs.AxisDirection; +import org.opengis.referencing.cs.CoordinateSystemAxis; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + + +/** + * Tests a few wrapper methods. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class WrappersTest { + /** + * Creates a new test case. + */ + public WrappersTest() { + } + + /** + * Tests the wrapping of a datum. + */ + @Test + public void testDatum() { + GeodeticDatum datum = Wrappers.geoapi(org.locationtech.proj4j.datum.Datum.NZGD49); + assertEquals("New Zealand Geodetic Datum 1949", datum.getName().getCode()); + assertEquals("nzgd49", datum.getAlias().iterator().next().toString()); + + Ellipsoid ellipsoid = datum.getEllipsoid(); + assertEquals("International 1909 (Hayford)", ellipsoid.getName().getCode()); + assertEquals("intl", ellipsoid.getAlias().iterator().next().toString()); + assertEquals(6378388, ellipsoid.getSemiMajorAxis(), 0); + assertEquals(6356911.95, ellipsoid.getSemiMinorAxis(), 0.005); + assertEquals(297, ellipsoid.getInverseFlattening(), 5E-10); + } + + /** + * Tests the creation of a geographic CRS. + */ + @Test + public void testGeographicCRS() { + final Unit degree = Units.getInstance().degree; + + final CRSFactory crsFactory = new CRSFactory(); + GeographicCRS crs = (GeographicCRS) Wrappers.geoapi(crsFactory.createFromName("EPSG:4326")); + assertEquals("EPSG:4326", crs.getName().getCode()); + + GeodeticDatum datum = crs.getDatum(); + assertEquals("WGS84", datum.getName().getCode()); + + PrimeMeridian pm = datum.getPrimeMeridian(); + assertEquals("greenwich", pm.getName().getCode()); + assertEquals(0, pm.getGreenwichLongitude(), 0); + assertEquals(degree, pm.getAngularUnit()); + + Ellipsoid ellipsoid = datum.getEllipsoid(); + assertEquals("WGS 84", ellipsoid.getName().getCode()); + assertEquals(6378137, ellipsoid.getSemiMajorAxis(), 0); + assertEquals(6356752.31, ellipsoid.getSemiMinorAxis(), 0.005); + assertEquals(298.257223563, ellipsoid.getInverseFlattening(), 5E-10); + + EllipsoidalCS cs = crs.getCoordinateSystem(); + assertEquals(2, cs.getDimension()); + + CoordinateSystemAxis axis = cs.getAxis(0); + assertEquals("Geodetic longitude", axis.getName().getCode()); + assertEquals("lon", axis.getAbbreviation()); + assertEquals(AxisDirection.EAST, axis.getDirection()); + assertEquals(degree, axis.getUnit()); + assertSame(axis, cs.getAxis(0)); + + axis = cs.getAxis(1); + assertEquals("Geodetic latitude", axis.getName().getCode()); + assertEquals("lat", axis.getAbbreviation()); + assertEquals(AxisDirection.NORTH, axis.getDirection()); + assertEquals(degree, axis.getUnit()); + assertSame(axis, cs.getAxis(1)); + } +} diff --git a/pom.xml b/pom.xml index c9f4f1e..aa59608 100644 --- a/pom.xml +++ b/pom.xml @@ -197,6 +197,7 @@ core epsg + geoapi From 6651f74218cfe9a3ce6eef7e35e1764c9373c137 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Mon, 10 Mar 2025 18:43:58 +0100 Subject: [PATCH 4/7] Add wrappers for Projected CRS and Coordinate Operation. Test the transform of a point from geographic to projected CRS. --- .../proj4j/geoapi/AbstractCRS.java | 29 +- .../proj4j/geoapi/GeographicCRSWrapper.java | 5 +- .../proj4j/geoapi/IdentifierEPSG.java | 82 ++++++ .../proj4j/geoapi/OperationMethodWrapper.java | 249 ++++++++++++++++ .../proj4j/geoapi/ParameterAccessor.java | 264 +++++++++++++++++ .../proj4j/geoapi/ParameterWrapper.java | 246 ++++++++++++++++ .../proj4j/geoapi/PositionWrapper.java | 208 ++++++++++++++ .../proj4j/geoapi/ProjectedCRSWrapper.java | 77 +++++ .../proj4j/geoapi/ProjectionWrapper2D.java | 54 ++++ .../proj4j/geoapi/ProjectionWrapper3D.java | 54 ++++ .../proj4j/geoapi/TransformWrapper.java | 217 ++++++++++++++ .../proj4j/geoapi/TransformWrapper2D.java | 272 ++++++++++++++++++ .../proj4j/geoapi/TransformWrapper3D.java | 202 +++++++++++++ .../locationtech/proj4j/geoapi/Wrapper.java | 45 ++- .../locationtech/proj4j/geoapi/Wrappers.java | 36 ++- .../proj4j/geoapi/WrappersTest.java | 132 ++++++++- 16 files changed, 2142 insertions(+), 30 deletions(-) create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/IdentifierEPSG.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectedCRSWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper2D.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper3D.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java index 9ef0a35..4b96651 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AbstractCRS.java @@ -28,21 +28,20 @@ /** * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. - * The CRS is assumed two-dimensional. * * @author Martin Desruisseaux (Geomatys) */ @SuppressWarnings("serial") abstract class AbstractCRS extends Wrapper implements SingleCRS, CoordinateSystem, Serializable { /** - * The number of dimensions of the CRS. + * The wrapped PROJ4 implementation. */ - private static final int BIDIMENSIONAL = 2; + final org.locationtech.proj4j.CoordinateReferenceSystem impl; /** - * The wrapped PROJ4 implementation. + * Whether this CRS is three-dimensional instead of two-dimensional. */ - private final org.locationtech.proj4j.CoordinateReferenceSystem impl; + final boolean is3D; /** * The coordinate system axes, computed and cached when first requested. @@ -55,23 +54,29 @@ abstract class AbstractCRS extends Wrapper implements SingleCRS, CoordinateSyste /** * Creates a new wrapper for the given PROJ4J implementation. */ - AbstractCRS(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { + AbstractCRS(final org.locationtech.proj4j.CoordinateReferenceSystem impl, final boolean is3D) { this.impl = impl; + this.is3D = is3D; } /** * Wraps the given implementation. * * @param impl the implementation to wrap, or {@code null} + * @param is3D whether to return a three-dimensional CRS instead of a two-dimensional one * @return the wrapper, or {@code null} if the given implementation was null */ - static AbstractCRS wrap(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { + static AbstractCRS wrap(final org.locationtech.proj4j.CoordinateReferenceSystem impl, final boolean is3D) { if (impl != null) { final Projection proj = impl.getProjection(); if (proj == null || proj.isGeographic()) { - return new GeographicCRSWrapper(impl); + return new GeographicCRSWrapper(impl, is3D); } else { - // TODO + /* + * TODO: there is a possibility that the PROJ4J `projection` is actually for something + * else than a map projection. But there is apparently no easy way to determine that. + */ + return new ProjectedCRSWrapper(impl, is3D); } } return null; @@ -111,11 +116,11 @@ public CoordinateSystem getCoordinateSystem() { } /** - * {@return the number of dimensions, which is fixed to 2}. + * {@return the number of dimensions, which is 2 or 3}. */ @Override public final int getDimension() { - return BIDIMENSIONAL; + return is3D ? TRIDIMENSIONAL : BIDIMENSIONAL; } /** @@ -147,7 +152,7 @@ public final CoordinateSystemAxis getAxis(int dimension) { Axis[] axes = this.axes; if (axes == null) { final Axis[] axesForAllDirections = axesForAllDirections(); - axes = Arrays.copyOfRange(axesForAllDirections, Axis.INDEX_OF_EAST, axesForAllDirections.length); + axes = Arrays.copyOfRange(axesForAllDirections, Axis.INDEX_OF_EAST, Axis.INDEX_OF_EAST + getDimension()); final Projection proj = impl.getProjection(); if (proj != null) { final AxisOrder order = proj.getAxisOrder(); diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java index e08f937..0f76e04 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/GeographicCRSWrapper.java @@ -21,7 +21,6 @@ /** * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. - * The CRS is assumed two-dimensional. * * @author Martin Desruisseaux (Geomatys) */ @@ -30,8 +29,8 @@ final class GeographicCRSWrapper extends AbstractCRS implements EllipsoidalCS, G /** * Creates a new wrapper for the given PROJ4J implementation. */ - GeographicCRSWrapper(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { - super(impl); + GeographicCRSWrapper(org.locationtech.proj4j.CoordinateReferenceSystem impl, boolean is3D) { + super(impl, is3D); } /** diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/IdentifierEPSG.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/IdentifierEPSG.java new file mode 100644 index 0000000..dde6508 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/IdentifierEPSG.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.util.Collections; +import java.util.Set; +import org.opengis.referencing.ReferenceIdentifier; + + +/** + * A simple EPSG identifier made of only a code and a code space. + * + * @author Martin Desruisseaux (Geomatys) + */ +final class IdentifierEPSG extends Wrapper { + /** + * The EPSG code. + */ + private final int code; + + /** + * Creates a new identifier for the given EPSG code. + */ + private IdentifierEPSG(final int code) { + this.code = code; + } + + /** + * Wraps the given EPSG code. + * + * @param code the EPSG code, or 0 if none + * @return the wrapper, or an empty set if the given EPSG code was 0 + */ + static Set wrap(final int code) { + return (code != 0) ? Collections.singleton(new IdentifierEPSG(code)) : Collections.emptySet(); + } + + /** + * {@return the EPSG code}. + */ + @Override + Object implementation() { + return code; + } + + /** + * {@return the code space, which is fixed to "EPSG"}. + */ + @Override + public String getCodeSpace() { + return "EPSG"; + } + + /** + * {@return the string representation of the EPSG code}. + */ + @Override + public String getCode() { + return Integer.toString(code); + } + + /** + * {@return the string representation of this identifier}. + */ + @Override + public String toString() { + return getCodeSpace() + ':' + code; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java new file mode 100644 index 0000000..8675b9f --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java @@ -0,0 +1,249 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.locationtech.proj4j.CoordinateReferenceSystem; +import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.proj.Projection; +import org.opengis.parameter.GeneralParameterDescriptor; +import org.opengis.parameter.GeneralParameterValue; +import org.opengis.parameter.ParameterDescriptorGroup; +import org.opengis.parameter.ParameterNotFoundException; +import org.opengis.parameter.ParameterValue; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.ReferenceIdentifier; +import org.opengis.referencing.operation.Formula; +import org.opengis.referencing.operation.OperationMethod; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class OperationMethodWrapper extends Wrapper implements OperationMethod, + ParameterDescriptorGroup, ParameterValueGroup, Serializable +{ + /** + * The wrapped PROJ4 implementation. + */ + private final Projection impl; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + private OperationMethodWrapper(final Projection impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static OperationMethodWrapper wrap(final Projection impl) { + return (impl != null) ? new OperationMethodWrapper(impl) : null; + } + + /** + * Wraps the target CRS of the given transform. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if none + */ + static OperationMethodWrapper wrapTarget(final CoordinateTransform impl) { + if (impl != null) { + CoordinateReferenceSystem crs = impl.getTargetCRS(); + if (crs != null) { + return wrap(crs.getProjection()); + } + } + return null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + final Object implementation() { + return impl; + } + + /** + * {@return the operation method name}. In the PROJ4J implementations, the {@link Projection#toString()} + * method seems to be the method name. However, this is not formalized in the Javadoc. + */ + @Override + public final String getCode() { + return impl.toString(); + } + + /** + * {@return the EPSG code of this method, if known}. + */ + @Override + public Set getIdentifiers() { + return IdentifierEPSG.wrap(impl.getEPSGCode()); + } + + /** + * Formula(s) or procedure used by this operation method. + * This information is not provided. + */ + @Override + public Formula getFormula() { + return null; + } + + /** + * @deprecated This property has been removed in latest revision of ISO 19111. + */ + @Override + @Deprecated + public Integer getSourceDimensions() { + return null; + } + + /** + * @deprecated This property has been removed in latest revision of ISO 19111. + */ + @Override + @Deprecated + public Integer getTargetDimensions() { + return null; + } + + /** + * {@return the minimum number of times that this parameter group is required}. + */ + @Override + public int getMinimumOccurs() { + return 1; + } + + /** + * {@return the maximum number of times that this parameter group is required}. + */ + @Override + public int getMaximumOccurs() { + return 1; + } + + /** + * {@return the descriptors of parameters of the projection}. + * This method is defined in {@link OperationMethod}. + */ + @Override + public ParameterDescriptorGroup getParameters() { + return this; + } + + /** + * {@return the descriptors of parameters of the projection}. + * This method is defined in {@link ParameterValueGroup}. + */ + @Override + public ParameterDescriptorGroup getDescriptor() { + return this; + } + + /** + * {@return the descriptions of all parameters having a non-default value}. + */ + @Override + public List descriptors() { + return Arrays.asList(ParameterAccessor.nonDefault(impl)); + } + + /** + * {@return the values of all parameters having a non-default value}. + */ + @Override + public List values() { + final ParameterAccessor[] descriptors = ParameterAccessor.nonDefault(impl); + final ParameterWrapper[] parameters = new ParameterWrapper[descriptors.length]; + for (int i=0; i parameter(String name) throws ParameterNotFoundException { + return new ParameterWrapper(impl, ParameterAccessor.forName(name)); + } + + /** + * Unsupported operation. + */ + @Override + public List groups(String name) throws ParameterNotFoundException { + throw new ParameterNotFoundException("Parameter groups are not supported.", name); + } + + /** + * Unsupported operation. + */ + @Override + public ParameterValueGroup addGroup(String name) throws ParameterNotFoundException, IllegalStateException { + throw new ParameterNotFoundException("Parameter groups are not supported.", name); + } + + /** + * Creates a new instance of this group of parameters. + * All accessible parameters are set to their default value. + */ + @Override + public ParameterValueGroup createValue() { + OperationMethodWrapper c = new OperationMethodWrapper((Projection) impl.clone()); + ParameterAccessor.reset(c.impl); + return c; + } + + /** + * {@return a copy of this group of parameters}. + */ + @Override + @SuppressWarnings("CloneDoesntCallSuperClone") + public ParameterValueGroup clone() { + return new OperationMethodWrapper((Projection) impl.clone()); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java new file mode 100644 index 0000000..f879490 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java @@ -0,0 +1,264 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Set; +import java.util.function.ToDoubleFunction; +import java.util.function.ObjDoubleConsumer; +import javax.measure.Unit; +import org.locationtech.proj4j.proj.NullProjection; +import org.locationtech.proj4j.proj.Projection; +import org.opengis.parameter.ParameterDescriptor; +import org.opengis.parameter.ParameterNotFoundException; +import org.opengis.parameter.ParameterValue; + + +/** + * Description of a PROJ4J parameter, together with method for getting and setting the value. + * This implementation is restricted to values of the {@code double} primitive type. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class ParameterAccessor extends Wrapper implements ParameterDescriptor, Serializable { + /** + * Parameters that we can extract from a {@link Projection} object. + * Does not include the ellipsoid axis length of flattening factors. + */ + private static final ParameterAccessor[] ACCESSORS = { + new ParameterAccessor("central_meridian", Projection::getProjectionLongitude, Projection::setProjectionLongitude, false, true), + new ParameterAccessor("latitude_of_origin", Projection::getProjectionLatitude, Projection::setProjectionLatitude, false, true), + new ParameterAccessor("standard_parallel_1", Projection::getProjectionLatitude1, Projection::setProjectionLatitude1, false, true), + new ParameterAccessor("standard_parallel_2", Projection::getProjectionLatitude2, Projection::setProjectionLatitude2, false, true), + new ParameterAccessor("true_scale_latitude", Projection::getTrueScaleLatitude, Projection::setTrueScaleLatitude, false, true), // Didn't found an OGC name for this one. + new ParameterAccessor("scale_factor", Projection::getScaleFactor, Projection::setScaleFactor, true, false), + new ParameterAccessor("false_easting", Projection::getFalseEasting, Projection::setFalseEasting, false, false), + new ParameterAccessor("false_northing", Projection::getFalseNorthing, Projection::setFalseNorthing, false, false) + }; + + /** + * The parameter name. Should be OGC names if possible. This name may not be correct in all cases, + * because some names depend on the projection method. For example, "latitude of origin" may be + * "latitude of center" in some projections. + */ + private final String name; + + /** + * The method to invoke for getting the parameter value. + */ + private final ToDoubleFunction getter; + + /** + * The method to invoke for setting the parameter value. + */ + private final ObjDoubleConsumer setter; + + /** + * Whether this parameter is the scale factor. + * That parameter has a different default value. + */ + private final boolean isScale; + + /** + * Whether the unit of measurement is angular (true) or linear (false). + */ + private final boolean angular; + + /** + * Creates a new parameter descriptor. + * + * @param name the parameter name + * @param getter the method to invoke for getting the parameter value + * @param setter the method to invoke for setting the parameter value + * @param isScale whether this parameter is the scale factor + * @param angular whether the unit of measurement is angular (true) or linear (false) + */ + private ParameterAccessor(final String name, + final ToDoubleFunction getter, + final ObjDoubleConsumer setter, + final boolean isScale, + final boolean angular) + { + this.name = name; + this.getter = getter; + this.setter = setter; + this.isScale = isScale; + this.angular = angular; + } + + /** + * Returns the parameter descriptor of the given name. + * + * @param name name of the desired parameter + * @return parameter descriptor for the given name + * @throws ParameterNotFoundException if the given name is unknown + */ + static ParameterAccessor forName(final String name) { + for (ParameterAccessor c : ACCESSORS) { + if (c.name.equalsIgnoreCase(name)) { + return c; + } + } + throw new ParameterNotFoundException("Parameter \"" + name + "\" is unknown or unsupported.", name); + } + + /** + * Returns all descriptors having a non-default values for the given PROJ4J projection. + * We do not have a formal list of parameters that are valid for each projection. + * Therefore, checking for non-default values is workaround. + */ + static ParameterAccessor[] nonDefault(final Projection proj) { + final ParameterAccessor[] parameters = new ParameterAccessor[ACCESSORS.length]; + int count = 0; + for (ParameterAccessor c : ACCESSORS) { + if (c.getter.applyAsDouble(proj) != c.defaultValue()) { + parameters[count++] = c; + } + } + return Arrays.copyOf(parameters, count); + } + + /** + * Resets all parameters to their default value. + */ + static void reset(final Projection proj) { + for (ParameterAccessor c : ACCESSORS) { + c.setter.accept(proj, c.defaultValue()); + } + } + + /** + * {@return an identification of the parameter}. + */ + @Override + Object implementation() { + return name; + } + + /** + * {@return the parameter name}. + */ + @Override + public String getCode() { + return name; + } + + /** + * {@return the class that describe the type of the parameter}. + */ + @Override + public Class getValueClass() { + return Double.class; + } + + /** + * {@return null as this parameter is not restricted to a limited set of values}. + */ + @Override + public Set getValidValues() { + return null; + } + + /** + * {@return the default value as initialized in the PROJ4 projection class}. + */ + @Override + public Double getDefaultValue() { + return defaultValue(); + } + + /** + * {@return the default value as a primitive type}. + */ + private double defaultValue() { + return isScale ? 1d : 0d; + } + + /** + * Unspecified. + */ + @Override + public Comparable getMinimumValue() { + return null; + } + + /** + * Unspecified. + */ + @Override + public Comparable getMaximumValue() { + return null; + } + + /** + * {@return the minimum number of times that values for this parameter are required}. + */ + @Override + public int getMinimumOccurs() { + return 1; + } + + /** + * {@return the maximum number of times that values for this parameter are required}. + */ + @Override + public int getMaximumOccurs() { + return 1; + } + + /** + * {@return the unit of measurement}. + */ + @Override + public Unit getUnit() { + final Units units = Units.getInstance(); + return isScale ? units.one : angular ? units.degree : units.metre; + } + + /** + * Gets the value of this parameter from the given projection. + */ + final double get(final Projection proj) { + double value = getter.applyAsDouble(proj); + if (angular) { + value = Math.toDegrees(value); + } + return value; + } + + /** + * Sets the value of this parameter in the given projection. + */ + final void set(final Projection proj, double value) { + if (angular) { + value = Math.toRadians(value); + } + setter.accept(proj, value); + } + + /** + * Creates a new parameter value. Note that this method is inefficient as it + * creates a full {@link Projection} object for each individual parameter value. + * + * @return a new parameter value + */ + @Override + public ParameterValue createValue() { + return new ParameterWrapper(new NullProjection(), this); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterWrapper.java new file mode 100644 index 0000000..c3f0e6b --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterWrapper.java @@ -0,0 +1,246 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.net.URI; +import java.util.AbstractMap; +import javax.measure.IncommensurableException; +import javax.measure.Unit; +import org.locationtech.proj4j.proj.Projection; +import org.opengis.parameter.InvalidParameterTypeException; +import org.opengis.parameter.InvalidParameterValueException; +import org.opengis.parameter.ParameterDescriptor; +import org.opengis.parameter.ParameterValue; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * This implementation is restricted to values of the {@code double} primitive type. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class ParameterWrapper extends Wrapper implements ParameterValue, Serializable { + /** + * The wrapped PROJ4 implementation. + */ + private final Projection impl; + + /** + * The parameter name together with the methods for getting or setting values. + */ + private final ParameterAccessor descriptor; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + * + * @param impl the wrapped PROJ4 implementation + * @param descriptor methods for getting or setting values + */ + ParameterWrapper(final Projection impl, final ParameterAccessor descriptor) { + this.impl = impl; + this.descriptor = descriptor; + } + + /** + * {@return an arbitrary object for equality and hash code}. + */ + @Override + Object implementation() { + return new AbstractMap.SimpleEntry<>(descriptor, impl); + } + + /** + * {@return a description of this parameter}. + */ + @Override + public ParameterDescriptor getDescriptor() { + return descriptor; + } + + /** + * {@return this parameter name, as specified in the descriptor}. + */ + @Override + public String getCode() { + return descriptor.getCode(); + } + + /** + * {@return the same unit of measurement as declared in the parameter descriptor}. + */ + @Override + public Unit getUnit() { + return descriptor.getUnit(); + } + + /** + * {@return the exception to throw for an illegal unit of measurement}. + */ + private IllegalArgumentException illegalUnit(IncommensurableException e) { + return new IllegalArgumentException("Illegal unit for the \"" + getCode() + "\" parameter.", e); + } + + /** + * {@return the exception to throw for all parameter types other than floating-point}. + */ + private InvalidParameterTypeException invalidReturnType() { + throw new InvalidParameterTypeException("The value can be provided only as a real number.", getCode()); + } + + /** + * {@return the exception to throw for all parameter types other than floating-point}. + */ + private InvalidParameterValueException invalidParamType(final Object value) { + throw new InvalidParameterValueException("The value can be set only as a real number.", getCode(), value); + } + + /** + * {@return the value as an arbitrary object}. + */ + @Override + public Double getValue() { + return doubleValue(); + } + + /** + * {@return the value in the PROJ4J projection for the parameter described by this object}. + */ + @Override + public double doubleValue() { + return descriptor.get(impl); + } + + /** + * {@return the value in the PROJ4J projection for the parameter described by this object}. + * The value is converted to the given unit of measurement. + * + * @param unit the unit of measurement of value to get + * @throws IllegalArgumentException if the given unit is incompatible + */ + @Override + public double doubleValue(Unit unit) { + try { + return getUnit().getConverterToAny(unit).convert(doubleValue()); + } catch (IncommensurableException e) { + throw illegalUnit(e); + } + } + + /** + * Sets the value as an arbitrary object. + * + * @param value the value to set + * @throws InvalidParameterValueException if the value type is illegal + */ + @Override + public void setValue(Object value) throws InvalidParameterValueException { + if (value instanceof Number) { + setValue(((Number) value).doubleValue()); + } + throw invalidParamType(value); + } + + /** + * Sets the value of this parameter. Note that invoking this method may modify the PROJ4J + * object wrapped by {@link OperationMethodWrapper}. This is generally not recommended. + * + * @param value the value to set + */ + @Override + public void setValue(double value) { + descriptor.set(impl, value); + } + + /** + * Converts the given value to the unit expected by this parameter, then sets the value. + * + * @param value the value to set + * @param unit the unit of measurement of the given value + * @throws UnsupportedOperationException if this parameter is unmodifiable + * @throws IllegalArgumentException if the given unit is incompatible + */ + @Override + public void setValue(double value, Unit unit) { + try { + setValue(unit.getConverterToAny(getUnit()).convert(value)); + } catch (IncommensurableException e) { + throw illegalUnit(e); + } + } + + @Override + public int intValue() { + throw invalidReturnType(); + } + + @Override + public boolean booleanValue() throws IllegalStateException { + throw invalidReturnType(); + } + + @Override + public String stringValue() throws IllegalStateException { + throw invalidReturnType(); + } + + @Override + public URI valueFile() throws IllegalStateException { + throw invalidReturnType(); + } + + @Override + public int[] intValueList() throws IllegalStateException { + throw invalidReturnType(); + } + + @Override + public double[] doubleValueList() throws IllegalStateException { + throw invalidReturnType(); + } + + @Override + public double[] doubleValueList(Unit unit) throws IllegalArgumentException, IllegalStateException { + throw invalidReturnType(); + } + + @Override + public void setValue(int value) throws InvalidParameterValueException { + throw invalidParamType(value); + } + + @Override + public void setValue(boolean value) throws InvalidParameterValueException { + throw invalidParamType(value); + } + + @Override + public void setValue(double[] values, Unit unit) throws InvalidParameterValueException { + throw invalidParamType(values); + } + + /** + * {@return a modifiable copy of this parameter}. Note that this method is inefficient + * as it creates a full {@link Projection} object for each individual parameter value. + * It is better to invoke {@link OperationMethodWrapper#clone()} instead. + */ + @Override + @SuppressWarnings("CloneDoesntCallSuperClone") + public ParameterValue clone() { + return new ParameterWrapper((Projection) impl.clone(), descriptor); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java new file mode 100644 index 0000000..0c4edd7 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java @@ -0,0 +1,208 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import org.locationtech.proj4j.ProjCoordinate; +import org.opengis.geometry.DirectPosition; +import org.opengis.geometry.MismatchedDimensionException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * The CRS is assumed two-dimensional. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class PositionWrapper extends Wrapper implements DirectPosition, Serializable { + /** + * The wrapped PROJ4 implementation. + */ + final ProjCoordinate impl; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + PositionWrapper(final ProjCoordinate impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static PositionWrapper wrap(final ProjCoordinate impl) { + return (impl != null) ? new PositionWrapper(impl) : null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + final Object implementation() { + return impl; + } + + /** + * Not applicable. + */ + @Override + public String getCode() { + return null; + } + + /** + * {@return the direct position, which is provided directly by this object}. + */ + @Override + public DirectPosition getDirectPosition() { + return this; + } + + /** + * Not specified. + */ + @Override + public CoordinateReferenceSystem getCoordinateReferenceSystem() { + return null; + } + + /** + * {@return the number of dimensions}, + * which is 2 or 3 depending on whether the z coordinate value is provided. + */ + @Override + public int getDimension() { + return Double.isNaN(impl.z) ? BIDIMENSIONAL : TRIDIMENSIONAL; + } + + /** + * {@return all coordinate values}. + */ + @Override + public double[] getCoordinate() { + final double[] coordinates = new double[getDimension()]; + coordinates[0] = impl.x; + coordinates[1] = impl.y; + if (coordinates.length >= TRIDIMENSIONAL) { + coordinates[2] = impl.z; + } + return coordinates; + } + + /** + * {@return the coordinate value in the given dimension}. + * + * @param dimension the dimension of the coordinate to get + */ + @Override + public double getOrdinate(int dimension) { + switch (dimension) { + case 0: return impl.x; + case 1: return impl.y; + case 2: return impl.z; + default: throw outOfBounds(dimension); + } + } + + /** + * Sets the coordinate value in the given dimension. + * + * @param dimension the dimension of the coordinate to set + * @param value the value to set + */ + @Override + public void setOrdinate(int dimension, double value) { + switch (dimension) { + case 0: impl.x = value; break; + case 1: impl.y = value; break; + case 2: impl.z = value; break; + default: throw outOfBounds(dimension); + } + } + + /** + * Copies the coordinates of the given PROJ4J object into the given GeoAPI object. + * + * @param src the source coordinates to copy + * @param tgt where to copy the coordinates + */ + @SuppressWarnings("fallthrough") + static void setLocation(final ProjCoordinate src, final DirectPosition tgt) { + if (tgt instanceof PositionWrapper) { + ProjCoordinate impl = ((PositionWrapper) tgt).impl; + if (impl != src) { // Otherwise nothing to do as the given target is already a view over the source. + impl.setValue(src); + } + } else { + final int dimension = tgt.getDimension(); + switch (dimension) { + default: throw unexpectedDimension(dimension); + case TRIDIMENSIONAL: tgt.setOrdinate(2, src.z); // Fall through + case BIDIMENSIONAL: tgt.setOrdinate(1, src.y); + tgt.setOrdinate(0, src.x); + } + } + } + + /** + * {@return the given position as a PROJ4J coordinate tuple}. + * This method tries to return the backing implementation if possible, + * or otherwise copies the coordinate values in a new coordinate tuple. + * + * @param src the position to unwrap or copy + */ + @SuppressWarnings("fallthrough") + static ProjCoordinate unwrapOrCopy(final DirectPosition src) { + if (src instanceof PositionWrapper) { + return ((PositionWrapper) src).impl; + } + ProjCoordinate tgt = new ProjCoordinate(); + final int dimension = src.getDimension(); + switch (dimension) { + default: throw unexpectedDimension(dimension); + case TRIDIMENSIONAL: tgt.z = src.getOrdinate(2); // Fall through + case BIDIMENSIONAL: tgt.y = src.getOrdinate(1); + tgt.x = src.getOrdinate(0); + } + return tgt; + } + + /** + * Returns the exception to throw for a coordinate dimension out of bounds. + * + * @param dimension the dimension which is out of bound + * @return the exception to throw + */ + private static IndexOutOfBoundsException outOfBounds(final int dimension) { + return new IndexOutOfBoundsException("Coordinate index " + dimension + " is out of bounds."); + } + + /** + * Constructs an exception for an unexpected number of dimensions. + * + * @param dimension the number of dimensions of the object provided by the user + * @return the exception to throw + */ + private static MismatchedDimensionException unexpectedDimension(final int dimension) { + return new MismatchedDimensionException("The given point has " + dimension + " dimensions while " + + (dimension <= BIDIMENSIONAL ? BIDIMENSIONAL : TRIDIMENSIONAL) + " dimensions were expected."); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectedCRSWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectedCRSWrapper.java new file mode 100644 index 0000000..c81ffad --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectedCRSWrapper.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.locationtech.proj4j.BasicCoordinateTransform; +import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.crs.ProjectedCRS; +import org.opengis.referencing.cs.CartesianCS; +import org.opengis.referencing.operation.Projection; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class ProjectedCRSWrapper extends AbstractCRS implements CartesianCS, ProjectedCRS { + /** + * The conversion from the base CRS, created when first requested. + */ + private transient Projection conversionFromBase; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + ProjectedCRSWrapper(org.locationtech.proj4j.CoordinateReferenceSystem impl, boolean is3D) { + super(impl, is3D); + } + + /** + * {@return the coordinate system, which is implemented by the same class for convenience}. + */ + @Override + public CartesianCS getCoordinateSystem() { + clearAxisCache(); + return this; + } + + @Override + final Axis[] axesForAllDirections() { + return Axis.PROJECTED; + } + + /** + * {@return the base CRS of this projected CRS}. + */ + @Override + public GeographicCRS getBaseCRS() { + return (GeographicCRS) getConversionFromBase().getSourceCRS(); + } + + /** + * {@return the conversion from the base CRS to this projected CRS}. + */ + @Override + public synchronized Projection getConversionFromBase() { + if (conversionFromBase == null) { + BasicCoordinateTransform tr = new BasicCoordinateTransform(impl.createGeographic(), impl); + conversionFromBase = is3D ? new ProjectionWrapper3D(tr) : new ProjectionWrapper2D(tr); + } + return conversionFromBase; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper2D.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper2D.java new file mode 100644 index 0000000..92333bf --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper2D.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.locationtech.proj4j.CoordinateTransform; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.operation.OperationMethod; + + +/** + * Wraps a PROJ4J transform behind the equivalent GeoAPI interface for the two-dimensional case of a map projection. + * The source CRS must be geographic and the target CRS must be projected. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class ProjectionWrapper2D extends TransformWrapper2D implements org.opengis.referencing.operation.Projection { + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + ProjectionWrapper2D(final CoordinateTransform impl) { + super(impl); + } + + /** + * {@return a description of the map projection}. + */ + @Override + public OperationMethod getMethod() { + return OperationMethodWrapper.wrapTarget(impl); + } + + /** + * {@return the parameters of the map projection}. + * In this implementation, this is provided by the same class as the description. + */ + @Override + public ParameterValueGroup getParameterValues() { + return OperationMethodWrapper.wrapTarget(impl); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper3D.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper3D.java new file mode 100644 index 0000000..21e1a55 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ProjectionWrapper3D.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.locationtech.proj4j.CoordinateTransform; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.operation.OperationMethod; + + +/** + * Wraps a PROJ4J transform behind the equivalent GeoAPI interface for the three-dimensional case of a map projection. + * The source CRS must be geographic and the target CRS must be projected. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class ProjectionWrapper3D extends TransformWrapper3D implements org.opengis.referencing.operation.Projection { + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + ProjectionWrapper3D(final CoordinateTransform impl) { + super(impl); + } + + /** + * {@return a description of the map projection}. + */ + @Override + public OperationMethod getMethod() { + return OperationMethodWrapper.wrapTarget(impl); + } + + /** + * {@return the parameters of the map projection}. + * In this implementation, this is provided by the same class as the description. + */ + @Override + public ParameterValueGroup getParameterValues() { + return OperationMethodWrapper.wrapTarget(impl); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper.java new file mode 100644 index 0000000..1cc0927 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper.java @@ -0,0 +1,217 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.Proj4jException; +import org.locationtech.proj4j.ProjCoordinate; +import org.locationtech.proj4j.proj.Projection; +import org.opengis.geometry.DirectPosition; +import org.opengis.metadata.quality.PositionalAccuracy; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.CoordinateOperation; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.Matrix; +import org.opengis.referencing.operation.TransformException; + + +/** + * Base class of two-dimensional or three-dimensional coordinate operation. + * The exact type of the operation (conversion, transformation or concatenated) is unknown. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +abstract class TransformWrapper extends Wrapper implements CoordinateOperation, MathTransform, Serializable { + /** + * The wrapped PROJ4 implementation. + */ + final CoordinateTransform impl; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + TransformWrapper(final CoordinateTransform impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @param is3D whether to return a three-dimensional operation instead of a two-dimensional one + * @return the wrapper, or {@code null} if the given implementation was null + */ + static TransformWrapper wrap(final CoordinateTransform impl, final boolean is3D) { + if (impl == null) { + return null; + } else if (is3D) { + return new TransformWrapper3D(impl); + } else { + return new TransformWrapper2D(impl); + } + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + final Object implementation() { + return impl; + } + + /** + * Returns the projection of the given CRS, or {@code null} if none. + */ + private static Projection getProjection(final org.locationtech.proj4j.CoordinateReferenceSystem crs) { + return (crs != null) ? crs.getProjection() : null; + } + + /** + * Returns the name of the given CRS, or an arbitrary name if none is specified. + */ + private static String getName(final org.locationtech.proj4j.CoordinateReferenceSystem crs) { + if (crs != null) { + String name = crs.getName(); + if (name != null) { + return name; + } + } + return "Unnamed"; + } + + /** + * {@return a name that summarizes the operation}. + */ + @Override + public String getCode() { + return getName(impl.getSourceCRS()) + " → " + getName(impl.getTargetCRS()); + } + + /** + * {@return the CRS of the source points}. + * May be {@code null} if unspecified. + */ + @Override + public final CoordinateReferenceSystem getSourceCRS() { + return AbstractCRS.wrap(impl.getSourceCRS(), getSourceDimensions() >= TRIDIMENSIONAL); + } + + /** + * {@return the CRS of the target points}. + * May be {@code null} if unspecified. + */ + @Override + public final CoordinateReferenceSystem getTargetCRS() { + return AbstractCRS.wrap(impl.getTargetCRS(), getTargetDimensions() >= TRIDIMENSIONAL); + } + + /** + * {@return the version of the coordinate transformation}. + * This is unknown by default. + */ + @Override + public String getOperationVersion() { + return null; + } + + /** + * {@return the impact of this operation on point accuracy}. + * This is unknown by default. + */ + @Override + public Collection getCoordinateOperationAccuracy() { + return Collections.emptyList(); + } + + /** + * {@return the object performing the actual coordinate operations}. + * This is the same object in the case of PROJ4J implementation. + */ + @Override + public final MathTransform getMathTransform() { + return this; + } + + /** + * Tests whether this transform does not move any points. + */ + @Override + public final boolean isIdentity() { + return Objects.equals(getProjection(impl.getSourceCRS()), + getProjection(impl.getTargetCRS())); + } + + /** + * Transforms the specified {@code ptSrc} and stores the result in {@code ptDst}. + * If the target position is a wrapper, this method writes the result directly in + * the backing implementation. This method has some flexibility on the number of + * dimensions (2 or 3). + */ + @Override + public final DirectPosition transform(DirectPosition ptSrc, DirectPosition ptDst) throws TransformException { + ProjCoordinate src = PositionWrapper.unwrapOrCopy(ptSrc); + ProjCoordinate tgt; + try { + if (ptDst instanceof PositionWrapper) { + tgt = ((PositionWrapper) ptDst).impl; + if (tgt == (tgt = impl.transform(src, tgt))) { + return ptDst; // Already a view over the PROJ4J coordinate tuple. + } + } else { + tgt = impl.transform(src, new ProjCoordinate()); + if (ptDst == null) { + return new PositionWrapper(tgt); + } + } + } catch (Proj4jException e) { + throw cannotTransform(e); + } + PositionWrapper.setLocation(tgt, ptDst); + return ptDst; + } + + /** + * Unsupported operation. + */ + @Override + public Matrix derivative(DirectPosition point) throws TransformException { + throw new TransformException("Derivatives are not supported."); + } + + /** + * validates the number of points argument. + */ + static void checkNumPts(final int numPts) { + if (numPts < 0) { + throw new IllegalArgumentException("Number of points shall be positive."); + } + } + + /** + * Wraps the given PROJ4J exception in a GeoAPI exception. + * + * @param e the PROJ4J exception + * @return the GeoAPI exception + */ + static TransformException cannotTransform(final Proj4jException e) { + return new TransformException(e.getMessage(), e); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java new file mode 100644 index 0000000..c9b99d7 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java @@ -0,0 +1,272 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.awt.Shape; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.util.Arrays; +import org.locationtech.proj4j.BasicCoordinateTransform; +import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.Proj4jException; +import org.locationtech.proj4j.ProjCoordinate; +import org.opengis.referencing.operation.MathTransform2D; +import org.opengis.referencing.operation.Matrix; +import org.opengis.referencing.operation.TransformException; + + +/** + * Wraps a PROJ4J transform behind the equivalent GeoAPI interface for the two-dimensional case. + * The exact type of the operation (conversion, transformation or concatenated) is unknown. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +class TransformWrapper2D extends TransformWrapper implements MathTransform2D { + /** + * The inverse of this wrapper, computed when first requested. + * + * @see #inverse() + */ + private transient TransformWrapper2D inverse; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + TransformWrapper2D(final CoordinateTransform impl) { + super(impl); + } + + /** + * {@return the number of dimensions of input coordinates, which is 2}. + * This number of dimensions is implied by the {@link MathTransform2D} + * interface implemented by this class. + */ + @Override + public final int getSourceDimensions() { + return BIDIMENSIONAL; + } + + /** + * {@return the number of dimensions of output coordinates, which is 2}. + * This number of dimensions is implied by the {@link MathTransform2D} + * interface implemented by this class. + */ + @Override + public final int getTargetDimensions() { + return BIDIMENSIONAL; + } + + /** + * Transforms the specified {@code ptSrc} and stores the result in {@code ptDst}. + */ + @Override + public Point2D transform(Point2D ptSrc, Point2D ptDst) throws TransformException { + ProjCoordinate src = new ProjCoordinate(ptSrc.getX(), ptSrc.getY()); + ProjCoordinate tgt = new ProjCoordinate(); + try { + tgt = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + if (ptDst == null) { + return new Point2D.Double(tgt.x, tgt.y); + } else { + ptDst.setLocation(tgt.x, tgt.y); + return ptDst; + } + } + + /** + * Transforms coordinate tuples in the given arrays in double precision. + * This is the most frequently used method. + */ + @Override + public void transform(double[] srcPts, int srcOff, + double[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + if (srcPts == dstPts && srcOff > dstOff) { + int end = srcOff + numPts * BIDIMENSIONAL; + if (end < dstOff) { + srcPts = Arrays.copyOfRange(srcPts, srcOff, end); + srcOff = 0; + } + } + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = result.x; + dstPts[dstOff++] = result.y; + } + } + + /** + * Transforms coordinate tuples in the given arrays in single precision. + * This is a copy of the double-precision variant of this method with only cast added. + */ + @Override + public void transform(float[] srcPts, int srcOff, + float[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + if (srcPts == dstPts && srcOff > dstOff) { + int end = srcOff + numPts * BIDIMENSIONAL; + if (end < dstOff) { + srcPts = Arrays.copyOfRange(srcPts, srcOff, end); + srcOff = 0; + } + } + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = (float) result.x; + dstPts[dstOff++] = (float) result.y; + } + } + + /** + * Transforms coordinate tuples in the given arrays, with source coordinates converted from single precision. + */ + @Override + public void transform(final float[] srcPts, int srcOff, + final double[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = result.x; + dstPts[dstOff++] = result.y; + } + } + + /** + * Transforms coordinate tuples in the given arrays, with target coordinates converted to single precision. + */ + @Override + public void transform(final double[] srcPts, int srcOff, + final float[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = (float) result.x; + dstPts[dstOff++] = (float) result.y; + } + } + + /** + * Transforms the given shape. This simple implementation transforms the control points. + * It does not check if some straight lines should be converted to curves. + */ + @Override + public Shape createTransformedShape(Shape shape) throws TransformException { + final PathIterator it = shape.getPathIterator(null); + final Path2D.Double path = new Path2D.Double(it.getWindingRule()); + final double[] buffer = new double[6]; + while (!it.isDone()) { + switch (it.currentSegment(buffer)) { + case PathIterator.SEG_CLOSE: { + path.closePath(); + break; + } + case PathIterator.SEG_MOVETO: { + transform(buffer, 0, buffer, 0, 1); + path.moveTo(buffer[0], buffer[1]); + break; + } + case PathIterator.SEG_LINETO: { + transform(buffer, 0, buffer, 0, 1); + path.lineTo(buffer[0], buffer[1]); + break; + } + case PathIterator.SEG_QUADTO: { + transform(buffer, 0, buffer, 0, 2); + path.quadTo(buffer[0], buffer[1], buffer[2], buffer[3]); + break; + } + case PathIterator.SEG_CUBICTO: { + transform(buffer, 0, buffer, 0, 3); + path.curveTo(buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]); + break; + } + } + it.next(); + } + return path; + } + + /** + * Unsupported operation. + */ + @Override + public Matrix derivative(Point2D point) throws TransformException { + throw new TransformException("Derivatives are not supported."); + } + + /** + * {@return the inverse of this coordinate operation}. + */ + @Override + public synchronized MathTransform2D inverse() { + TransformWrapper2D cached = inverse; + if (cached == null) { + if (isIdentity()) { + cached = this; + } else { + cached = new TransformWrapper2D(new BasicCoordinateTransform(impl.getTargetCRS(), impl.getSourceCRS())); + cached.inverse = this; + } + inverse = cached; + } + return cached; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java new file mode 100644 index 0000000..3281fd5 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java @@ -0,0 +1,202 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.util.Arrays; +import org.locationtech.proj4j.BasicCoordinateTransform; +import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.Proj4jException; +import org.locationtech.proj4j.ProjCoordinate; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.TransformException; + + +/** + * Wraps a PROJ4J transform behind the equivalent GeoAPI interface for the three-dimensional case. + * The exact type of the operation (conversion, transformation or concatenated) is unknown. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +class TransformWrapper3D extends TransformWrapper { + /** + * The inverse of this wrapper, computed when first requested. + * + * @see #inverse() + */ + private transient TransformWrapper3D inverse; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + TransformWrapper3D(final CoordinateTransform impl) { + super(impl); + } + + /** + * {@return the number of dimensions of input coordinates, which is 3}. + */ + @Override + public final int getSourceDimensions() { + return TRIDIMENSIONAL; + } + + /** + * {@return the number of dimensions of output coordinates, which is 3}. + */ + @Override + public final int getTargetDimensions() { + return TRIDIMENSIONAL; + } + + /** + * Transforms coordinate tuples in the given arrays in double precision. + * This is the most frequently used method. + */ + @Override + public void transform(double[] srcPts, int srcOff, + double[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + if (srcPts == dstPts && srcOff > dstOff) { + int end = srcOff + numPts * TRIDIMENSIONAL; + if (end < dstOff) { + srcPts = Arrays.copyOfRange(srcPts, srcOff, end); + srcOff = 0; + } + } + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + src.z = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = result.x; + dstPts[dstOff++] = result.y; + dstPts[dstOff++] = result.z; + } + } + + /** + * Transforms coordinate tuples in the given arrays in single precision. + * This is a copy of the double-precision variant of this method with only cast added. + */ + @Override + public void transform(float[] srcPts, int srcOff, + float[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + if (srcPts == dstPts && srcOff > dstOff) { + int end = srcOff + numPts * TRIDIMENSIONAL; + if (end < dstOff) { + srcPts = Arrays.copyOfRange(srcPts, srcOff, end); + srcOff = 0; + } + } + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + src.z = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = (float) result.x; + dstPts[dstOff++] = (float) result.y; + dstPts[dstOff++] = (float) result.z; + } + } + + /** + * Transforms coordinate tuples in the given arrays, with source coordinates converted from single precision. + */ + @Override + public void transform(final float[] srcPts, int srcOff, + final double[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + src.z = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = result.x; + dstPts[dstOff++] = result.y; + dstPts[dstOff++] = result.z; + } + } + + /** + * Transforms coordinate tuples in the given arrays, with target coordinates converted to single precision. + */ + @Override + public void transform(final double[] srcPts, int srcOff, + final float[] dstPts, int dstOff, int numPts) throws TransformException + { + checkNumPts(numPts); + final ProjCoordinate src = new ProjCoordinate(); + final ProjCoordinate tgt = new ProjCoordinate(); + ProjCoordinate result; + while (--numPts >= 0) { + src.x = srcPts[srcOff++]; + src.y = srcPts[srcOff++]; + src.z = srcPts[srcOff++]; + try { + result = impl.transform(src, tgt); + } catch (Proj4jException e) { + throw cannotTransform(e); + } + dstPts[dstOff++] = (float) result.x; + dstPts[dstOff++] = (float) result.y; + dstPts[dstOff++] = (float) result.z; + } + } + + /** + * {@return the inverse of this coordinate operation}. + */ + @Override + public synchronized MathTransform inverse() { + TransformWrapper3D cached = inverse; + if (cached == null) { + if (isIdentity()) { + cached = this; + } else { + cached = new TransformWrapper3D(new BasicCoordinateTransform(impl.getTargetCRS(), impl.getSourceCRS())); + cached.inverse = this; + } + inverse = cached; + } + return cached; + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java index 36696cf..0b58a6e 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrapper.java @@ -32,7 +32,17 @@ * * @author Martin Desruisseaux (Geomatys) */ -abstract class Wrapper implements IdentifiedObject, ReferenceIdentifier { +abstract class Wrapper implements ReferenceIdentifier { + /** + * The default number of dimensions of the CRS. + */ + static final int BIDIMENSIONAL = 2; + + /** + * The number of dimensions when a non-NaN z coordinate value is provided. + */ + static final int TRIDIMENSIONAL = 3; + /** * Creates a new wrapper. */ @@ -84,8 +94,11 @@ public String getVersion() { * {@return the primary object name}. This method returns {@code this}, * with the expectation that users will follow with {@link #getCode()}. * Subclasses shall return the actual object name in {@code getCode()}. + * + *

This is a method declared in the {@link IdentifiedObject} interface. + * It is implemented in this base class for the convenience of subclasses + * that indirectly implement {@code IdentifiedObject}.

*/ - @Override public final ReferenceIdentifier getName() { return this; } @@ -94,8 +107,11 @@ public final ReferenceIdentifier getName() { * {@return other names of this object}. * In the EPSG database, this is usually the short name. * The default implementation assumes that there is none. + * + *

This is a method declared in the {@link IdentifiedObject} interface. + * It is implemented in this base class for the convenience of subclasses + * that indirectly implement {@code IdentifiedObject}.

*/ - @Override public Collection getAlias() { return Collections.emptyList(); } @@ -103,8 +119,11 @@ public Collection getAlias() { /** * {@return all identifiers (usually EPSG codes) of this object}. * The default implementation assumes that there is none. + * + *

This is a method declared in the {@link IdentifiedObject} interface. + * It is implemented in this base class for the convenience of subclasses + * that indirectly implement {@code IdentifiedObject}.

*/ - @Override public Set getIdentifiers() { return Collections.emptySet(); } @@ -112,6 +131,9 @@ public Set getIdentifiers() { /** * {@return the scope of usage of this object}. * If unknown, ISO 19111 requires that we return "not known". + * + *

This method is not declared directly in the {@link IdentifiedObject} interface, + * but appears in datum and coordinate operation sub-interfaces.

*/ public InternationalString getScope() { return LocalizedString.UNKNOWN; @@ -120,6 +142,9 @@ public InternationalString getScope() { /** * {@return the domain of validity of this object}. * The default implementation assumes that there is none. + * + *

This method is not declared directly in the {@link IdentifiedObject} interface, + * but appears in datum and coordinate operation sub-interfaces.

*/ public Extent getDomainOfValidity() { return null; @@ -128,8 +153,11 @@ public Extent getDomainOfValidity() { /** * {@return optional remarks about this object}. * The default implementation assumes that there is none. + * + *

This is a method declared in the {@link IdentifiedObject} interface. + * It is implemented in this base class for the convenience of subclasses + * that indirectly implement {@code IdentifiedObject}.

*/ - @Override public InternationalString getRemarks() { return null; } @@ -137,8 +165,11 @@ public InternationalString getRemarks() { /** * {@return a WKT representation of this object}. * The default implementation assumes that there is none. + * + *

This is a method declared in the {@link IdentifiedObject} interface. + * It is implemented in this base class for the convenience of subclasses + * that indirectly implement {@code IdentifiedObject}.

*/ - @Override public String toWKT() throws UnsupportedOperationException { throw new UnsupportedOperationException("Not supported."); } @@ -147,7 +178,7 @@ public String toWKT() throws UnsupportedOperationException { * {@return the string representation of the wrapped PROJ4J object}. */ @Override - public final String toString() { + public String toString() { return implementation().toString(); } diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java index 3f8e788..449112d 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java @@ -15,12 +15,16 @@ */ package org.locationtech.proj4j.geoapi; +import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.ProjCoordinate; +import org.opengis.geometry.DirectPosition; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.crs.SingleCRS; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.datum.GeodeticDatum; import org.opengis.referencing.datum.PrimeMeridian; +import org.opengis.referencing.operation.CoordinateOperation; /** @@ -46,10 +50,11 @@ private Wrappers() { * be changed after construction.

* * @param impl the implementation to wrap, or {@code null} + * @param is3D whether to return a three-dimensional CRS instead of a two-dimensional one * @return the view, or {@code null} if the given implementation was null */ - public static SingleCRS geoapi(final org.locationtech.proj4j.CoordinateReferenceSystem impl) { - return AbstractCRS.wrap(impl); + public static SingleCRS geoapi(final org.locationtech.proj4j.CoordinateReferenceSystem impl, boolean is3D) { + return AbstractCRS.wrap(impl, is3D); } /** @@ -90,4 +95,31 @@ public static Ellipsoid geoapi(final org.locationtech.proj4j.datum.Ellipsoid imp public static PrimeMeridian geoapi(final org.locationtech.proj4j.datum.PrimeMeridian impl) { return PrimeMeridianWrapper.wrap(impl); } + + /** + * Wraps the given PROJ4J coordinate transform behind the equivalent GeoAPI interface. + * The returned object is a view: if any {@code impl} value is changed after this method call, + * those changes will be reflected immediately in the returned view. Note that referencing objects + * should be immutable. Therefore, it is recommended to not apply any change on {@code impl}. + * + * @param impl the implementation to wrap, or {@code null} + * @param is3D whether to return a three-dimensional operation instead of a two-dimensional one + * @return the view, or {@code null} if the given implementation was null + */ + public static CoordinateOperation geoapi(final CoordinateTransform impl, final boolean is3D) { + return TransformWrapper.wrap(impl, is3D); + } + + /** + * Wraps the given PROJ4J coordinate tuple behind the equivalent GeoAPI interface. + * The returned object is a view: if any {@code impl} value is changed after this method call, + * those changes will be reflected immediately in the returned view. Conversely, setting a value in the + * returned view set the corresponding value in the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static DirectPosition geoapi(final ProjCoordinate impl) { + return PositionWrapper.wrap(impl); + } } diff --git a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java index 513dfaf..03b1d85 100644 --- a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java +++ b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java @@ -17,17 +17,30 @@ import javax.measure.Unit; import javax.measure.quantity.Angle; +import javax.measure.quantity.Length; import org.junit.Test; import org.locationtech.proj4j.CRSFactory; +import org.locationtech.proj4j.ProjCoordinate; +import org.opengis.geometry.DirectPosition; +import org.opengis.parameter.ParameterValue; +import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.datum.GeodeticDatum; import org.opengis.referencing.datum.PrimeMeridian; import org.opengis.referencing.datum.Ellipsoid; +import org.opengis.referencing.cs.CartesianCS; import org.opengis.referencing.cs.EllipsoidalCS; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CoordinateSystemAxis; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.OperationMethod; +import org.opengis.referencing.operation.Projection; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import org.opengis.referencing.operation.TransformException; /** @@ -61,30 +74,38 @@ public void testDatum() { /** * Tests the creation of a geographic CRS. + * This method verifies the datum (including its dependencies) and the coordinate system. */ @Test public void testGeographicCRS() { final Unit degree = Units.getInstance().degree; - final CRSFactory crsFactory = new CRSFactory(); - GeographicCRS crs = (GeographicCRS) Wrappers.geoapi(crsFactory.createFromName("EPSG:4326")); + final GeographicCRS crs = (GeographicCRS) Wrappers.geoapi(crsFactory.createFromName("EPSG:4326"), false); assertEquals("EPSG:4326", crs.getName().getCode()); - GeodeticDatum datum = crs.getDatum(); + /* + * First property of a CRS: the datum, which includes the ellipsoid and the prime meridian. + * Verify the name, Greenwich longitude, ellipsoid axis lengths and units of measurement. + */ + final GeodeticDatum datum = crs.getDatum(); assertEquals("WGS84", datum.getName().getCode()); - PrimeMeridian pm = datum.getPrimeMeridian(); + final PrimeMeridian pm = datum.getPrimeMeridian(); assertEquals("greenwich", pm.getName().getCode()); assertEquals(0, pm.getGreenwichLongitude(), 0); assertEquals(degree, pm.getAngularUnit()); - Ellipsoid ellipsoid = datum.getEllipsoid(); + final Ellipsoid ellipsoid = datum.getEllipsoid(); assertEquals("WGS 84", ellipsoid.getName().getCode()); assertEquals(6378137, ellipsoid.getSemiMajorAxis(), 0); assertEquals(6356752.31, ellipsoid.getSemiMinorAxis(), 0.005); assertEquals(298.257223563, ellipsoid.getInverseFlattening(), 5E-10); - EllipsoidalCS cs = crs.getCoordinateSystem(); + /* + * Second property of a CRS: its coordinate system. + * Verify axis name, abbreviation, direction and unit. + */ + final EllipsoidalCS cs = crs.getCoordinateSystem(); assertEquals(2, cs.getDimension()); CoordinateSystemAxis axis = cs.getAxis(0); @@ -101,4 +122,103 @@ public void testGeographicCRS() { assertEquals(degree, axis.getUnit()); assertSame(axis, cs.getAxis(1)); } + + /** + * Tests the creation of a projected CRS. + * This method verifies the datum, the coordinate system and the projection parameters. + * Opportunistically tests the transformation of a point. + * + * @throws TransformException if an error occurred while testing the projection of a point + */ + @Test + public void testProjectedCRS() throws TransformException { + final Unit degree = Units.getInstance().degree; + final Unit metre = Units.getInstance().metre; + final CRSFactory crsFactory = new CRSFactory(); + final ProjectedCRS crs = (ProjectedCRS) Wrappers.geoapi(crsFactory.createFromName("EPSG:2154"), false); + assertEquals("EPSG:2154", crs.getName().getCode()); + + /* + * First property of a CRS: the datum, which includes the ellipsoid and the prime meridian. + * Verify the name, Greenwich longitude, ellipsoid axis lengths and units of measurement. + */ + final GeodeticDatum datum = crs.getDatum(); + final PrimeMeridian pm = datum.getPrimeMeridian(); + assertEquals("greenwich", pm.getName().getCode()); + assertEquals(0, pm.getGreenwichLongitude(), 0); + assertEquals(degree, pm.getAngularUnit()); + + final Ellipsoid ellipsoid = datum.getEllipsoid(); + assertTrue(ellipsoid.getName().getCode().startsWith("GRS 1980")); + assertEquals(6378137, ellipsoid.getSemiMajorAxis(), 0); + assertEquals(6356752.31, ellipsoid.getSemiMinorAxis(), 0.005); + assertEquals(298.257222101, ellipsoid.getInverseFlattening(), 5E-10); + + /* + * Second property of a CRS: its coordinate system. + * Verify axis name, abbreviation, direction and unit. + */ + final CartesianCS cs = crs.getCoordinateSystem(); + assertEquals(2, cs.getDimension()); + + CoordinateSystemAxis axis = cs.getAxis(0); + assertEquals("Easting", axis.getName().getCode()); + assertEquals("E", axis.getAbbreviation()); + assertEquals(AxisDirection.EAST, axis.getDirection()); + assertEquals(metre, axis.getUnit()); + assertSame(axis, cs.getAxis(0)); + + axis = cs.getAxis(1); + assertEquals("Northing", axis.getName().getCode()); + assertEquals("N", axis.getAbbreviation()); + assertEquals(AxisDirection.NORTH, axis.getDirection()); + assertEquals(metre, axis.getUnit()); + assertSame(axis, cs.getAxis(1)); + + /* + * Property specific to a projected CRS: conversion from the base CRS. + * Verify parameters having a value different than their default value. + */ + final GeographicCRS baseCRS = crs.getBaseCRS(); + assertEquals(datum, baseCRS.getDatum()); + + final Projection conversionFromBase = crs.getConversionFromBase(); + final OperationMethod method = conversionFromBase.getMethod(); + assertArrayEquals(new String[] { + "central_meridian", + "latitude_of_origin", + "standard_parallel_1", + "standard_parallel_2", + "false_easting", + "false_northing" + }, method.getParameters().descriptors().stream().map((d) -> d.getName().getCode()).toArray()); + + final ParameterValueGroup pv = conversionFromBase.getParameterValues(); + assertEquals( 46.5, pv.parameter("latitude_of_origin") .doubleValue(), 1E-12); + assertEquals( 3.0, pv.parameter("central_meridian") .doubleValue(), 1E-12); + assertEquals( 49.0, pv.parameter("standard_parallel_1").doubleValue(), 1E-12); + assertEquals( 44.0, pv.parameter("standard_parallel_2").doubleValue(), 1E-12); + assertEquals( 700000.0, pv.parameter("false_easting") .doubleValue(), 0); + assertEquals(6600000.0, pv.parameter("false_northing") .doubleValue(), 0); + + // Test unit conversion. + final ParameterValue origin = pv.parameter("latitude_of_origin"); + assertEquals(46.5, origin.doubleValue(degree), 1E-12); + assertEquals(Math.toRadians(46.5), origin.doubleValue(Units.getInstance().radian), 1E-12); + + /* + * Test the transform of a point, then test the inverse operation. + */ + final MathTransform tr = conversionFromBase.getMathTransform(); + DirectPosition pt = Wrappers.geoapi(new ProjCoordinate(3, 46.5)); + assertEquals(2, pt.getDimension()); + pt = tr.transform(pt, pt); + assertEquals(2, pt.getDimension()); + assertEquals( 700000, pt.getOrdinate(0), 1E-3); + assertEquals(6600000, pt.getOrdinate(1), 1E-3); + pt = tr.inverse().transform(pt, pt); + assertEquals(2, pt.getDimension()); + assertEquals(46.5, pt.getOrdinate(1), 1E-9); + assertEquals( 3.0, pt.getOrdinate(0), 1E-9); + } } From e642a127658f32b51a792098a8150af483ba6146 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Tue, 11 Mar 2025 16:10:12 +0100 Subject: [PATCH 5/7] =?UTF-8?q?Add=20wrappers=20for=20factories=20and=20re?= =?UTF-8?q?gister=20them=20in=20META-INF/services.=20A=20side-effect=20of?= =?UTF-8?q?=20this=20work=20is=20the=20addition=20of=20`proj4j(=E2=80=A6)`?= =?UTF-8?q?=20methods=20as=20the=20reverse=20of=20`geoapi(=E2=80=A6)`=20me?= =?UTF-8?q?thods.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geoapi/pom.xml | 1 + .../geoapi/AuthorityFactoryWrapper.java | 196 ++++++++ .../proj4j/geoapi/DatumWrapper.java | 2 +- .../proj4j/geoapi/EllipsoidWrapper.java | 2 +- .../locationtech/proj4j/geoapi/Importer.java | 459 ++++++++++++++++++ .../geoapi/OperationFactoryWrapper.java | 161 ++++++ .../proj4j/geoapi/OperationMethodWrapper.java | 6 +- .../proj4j/geoapi/ParameterAccessor.java | 7 +- .../proj4j/geoapi/PositionWrapper.java | 3 + .../proj4j/geoapi/PrimeMeridianWrapper.java | 2 +- .../proj4j/geoapi/SimpleCitation.java | 270 +++++++++++ .../UnconvertibleInstanceException.java | 55 +++ .../org/locationtech/proj4j/geoapi/Units.java | 24 + .../locationtech/proj4j/geoapi/Wrappers.java | 190 ++++++++ .../proj4j/geoapi/package-info.java | 15 + .../proj4j/geoapi/spi/AuthorityFactory.java | 135 ++++++ .../proj4j/geoapi/spi/OperationFactory.java | 85 ++++ .../proj4j/geoapi/spi/package-info.java | 24 + ...pengis.referencing.crs.CRSAuthorityFactory | 1 + ...ncing.operation.CoordinateOperationFactory | 1 + .../proj4j/geoapi/ServicesTest.java | 79 +++ .../proj4j/geoapi/WrappersTest.java | 31 +- 22 files changed, 1734 insertions(+), 15 deletions(-) create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/AuthorityFactoryWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/Importer.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationFactoryWrapper.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/SimpleCitation.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/UnconvertibleInstanceException.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/package-info.java create mode 100644 geoapi/src/main/resources/META-INF/services/org.opengis.referencing.crs.CRSAuthorityFactory create mode 100644 geoapi/src/main/resources/META-INF/services/org.opengis.referencing.operation.CoordinateOperationFactory create mode 100644 geoapi/src/test/java/org/locationtech/proj4j/geoapi/ServicesTest.java diff --git a/geoapi/pom.xml b/geoapi/pom.xml index 80731e4..f458c16 100644 --- a/geoapi/pom.xml +++ b/geoapi/pom.xml @@ -64,6 +64,7 @@ https://www.geoapi.org/3.0/javadoc/ + org.locationtech.proj4j.geoapi.spi
diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AuthorityFactoryWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AuthorityFactoryWrapper.java new file mode 100644 index 0000000..a37ae78 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/AuthorityFactoryWrapper.java @@ -0,0 +1,196 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Set; +import org.locationtech.proj4j.CRSFactory; +import org.locationtech.proj4j.Proj4jException; +import org.locationtech.proj4j.UnknownAuthorityCodeException; +import org.opengis.metadata.citation.Citation; +import org.opengis.referencing.IdentifiedObject; +import org.opengis.referencing.NoSuchAuthorityCodeException; +import org.opengis.referencing.crs.*; +import org.opengis.util.FactoryException; +import org.opengis.util.InternationalString; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class AuthorityFactoryWrapper extends Wrapper implements CRSAuthorityFactory, Serializable { + /** + * The wrapped PROJ4 implementation. + */ + final CRSFactory impl; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + private AuthorityFactoryWrapper(final CRSFactory impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static AuthorityFactoryWrapper wrap(final CRSFactory impl) { + return (impl != null) ? new AuthorityFactoryWrapper(impl) : null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + Object implementation() { + return impl; + } + + /** + * {@return the factory name}. + */ + @Override + public String getCode() { + return "PROJ4J"; + } + + /** + * {@return an identification of the softwware that provides the CRS definitions}. + * This is not the authority (EPSG, ESRI, etc). + */ + @Override + public Citation getVendor() { + return SimpleCitation.PROJ4J; + } + + /** + * Returns the name of the CRS for the given code. Usually, this method is for fetching the name without the + * cost of creating the full CRS. However, this implementation is inefficient in this regard. + */ + @Override + public InternationalString getDescriptionText(String code) throws FactoryException { + return LocalizedString.wrap(createCoordinateReferenceSystem(code).getName().getCode()); + } + + /** + * Generic method defined in parent interface. + */ + @Override + public IdentifiedObject createObject(String code) throws FactoryException { + return createCoordinateReferenceSystem(code); + } + + /** + * Creates a CRS from a code in the {@code "AUTHORITY:CODE"} syntax. + * If the authority is unspecified, then {@code "EPSG"} is assumed. + * + * @param code the authority (optional) and code of the CRS to create + * @return the CRS for the given code + * @throws FactoryException if the CRS cannot be created + */ + @Override + public CoordinateReferenceSystem createCoordinateReferenceSystem(String code) throws FactoryException { + try { + return AbstractCRS.wrap(impl.createFromName(code), false); + } catch (UnknownAuthorityCodeException e) { + final int s = code.indexOf(':'); + throw (NoSuchAuthorityCodeException) new NoSuchAuthorityCodeException( + "No registered CRS for \"" + code + "\".", + (s >= 0) ? code.substring(0, s).trim() : null, + (s >= 0) ? code.substring(s).trim() : code, code).initCause(e); + } catch (Proj4jException e) { + throw new FactoryException("Cannot create a CRS for \"" + code + "\".", e); + } + } + + /** + * Creates the CRS from the specified code and cast to a geographic CRS. + * + * @param code the authority (optional) and code of the CRS to create + * @return the CRS for the given code + * @throws FactoryException if the CRS cannot be created or is not geographic + */ + @Override + public GeographicCRS createGeographicCRS(String code) throws FactoryException { + try { + return (GeographicCRS) createCoordinateReferenceSystem(code); + } catch (ClassCastException e) { + throw new FactoryException("The CRS identified by \"" + code + "\" is not geographic.", e); + } + } + + /** + * Creates the CRS from the specified code and cast to a projected CRS. + * + * @param code the authority (optional) and code of the CRS to create + * @return the CRS for the given code + * @throws FactoryException if the CRS cannot be created or is not projected + */ + @Override + public ProjectedCRS createProjectedCRS(String code) throws FactoryException { + try { + return (ProjectedCRS) createCoordinateReferenceSystem(code); + } catch (ClassCastException e) { + throw new FactoryException("The CRS identified by \"" + code + "\" is not projected.", e); + } + } + + @Override + public GeocentricCRS createGeocentricCRS(String code) throws NoSuchAuthorityCodeException, FactoryException { + throw new FactoryException("Not implemented."); + } + + @Override + public VerticalCRS createVerticalCRS(String code) throws NoSuchAuthorityCodeException, FactoryException { + throw new FactoryException("Not implemented."); + } + + @Override + public TemporalCRS createTemporalCRS(String code) throws NoSuchAuthorityCodeException, FactoryException { + throw new FactoryException("Not implemented."); + } + + @Override + public EngineeringCRS createEngineeringCRS(String code) throws NoSuchAuthorityCodeException, FactoryException { + throw new FactoryException("Not implemented."); + } + + @Override + public ImageCRS createImageCRS(String code) throws NoSuchAuthorityCodeException, FactoryException { + throw new FactoryException("Not implemented."); + } + + @Override + public DerivedCRS createDerivedCRS(String code) throws NoSuchAuthorityCodeException, FactoryException { + throw new FactoryException("Not implemented."); + } + + @Override + public CompoundCRS createCompoundCRS(String code) throws FactoryException { + throw new FactoryException("Not implemented."); + } + + @Override + public Set getAuthorityCodes(Class type) throws FactoryException { + throw new FactoryException("Not implemented."); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java index 32b7fa1..f0b1394 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/DatumWrapper.java @@ -36,7 +36,7 @@ final class DatumWrapper extends Wrapper implements GeodeticDatum, Serializable /** * The wrapped PROJ4 implementation. */ - private final org.locationtech.proj4j.datum.Datum impl; + final org.locationtech.proj4j.datum.Datum impl; /** * The prime meridian, or {@code null} for Greenwich diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java index c61efab..6b0d95a 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/EllipsoidWrapper.java @@ -33,7 +33,7 @@ final class EllipsoidWrapper extends Wrapper implements Ellipsoid, Serializable /** * The wrapped PROJ4 implementation. */ - private final org.locationtech.proj4j.datum.Ellipsoid impl; + final org.locationtech.proj4j.datum.Ellipsoid impl; /** * Creates a new wrapper for the given PROJ4J implementation. diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Importer.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Importer.java new file mode 100644 index 0000000..8109c01 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Importer.java @@ -0,0 +1,459 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import javax.measure.Unit; +import javax.measure.UnitConverter; +import org.locationtech.proj4j.CRSFactory; +import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.CoordinateTransformFactory; +import org.locationtech.proj4j.Registry; +import org.locationtech.proj4j.datum.AxisOrder; +import org.locationtech.proj4j.proj.LongLatProjection; +import org.locationtech.proj4j.proj.Projection; +import org.opengis.metadata.citation.Citation; +import org.opengis.parameter.GeneralParameterValue; +import org.opengis.parameter.ParameterValue; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.IdentifiedObject; +import org.opengis.referencing.ReferenceIdentifier; +import org.opengis.referencing.crs.CRSAuthorityFactory; +import org.opengis.referencing.crs.GeographicCRS; +import org.opengis.referencing.crs.ProjectedCRS; +import org.opengis.referencing.crs.SingleCRS; +import org.opengis.referencing.cs.AxisDirection; +import org.opengis.referencing.cs.CoordinateSystem; +import org.opengis.referencing.datum.Ellipsoid; +import org.opengis.referencing.datum.GeodeticDatum; +import org.opengis.referencing.datum.PrimeMeridian; +import org.opengis.referencing.operation.CoordinateOperation; +import org.opengis.referencing.operation.CoordinateOperationFactory; +import org.opengis.util.Factory; +import org.opengis.util.GenericName; +import org.opengis.util.InternationalString; +import org.opengis.util.NameSpace; + + +/** + * Builder of PROJ4J objects from GeoAPI objects. If the GeoAPI object has been created by a + * call to a {@code Wrappers.geoapi(…)} method, then the wrapped object is returned directly. + * Otherwise, this class tries to creates new PROJ4J instances using the information provided + * in the GeoAPI object. It may fail, in which case an {@link UnconvertibleInstanceException} + * is thrown. + * + * @author Martin Desruisseaux (Geomatys) + */ +public class Importer { + /** + * Possible name spaces for PROJ4J operation methods, case-insensitive. + */ + private static final String[] PROJ_NAMESPACES = {"PROJ", "PROJ4", "PROJ.4", "PROJ4J"}; + + /** + * Possible name spaces for OGC parameters, case-insensitive. + * ESRI parameters are usually the same as OGC parameters except for the case. + */ + private static final String[] OGC_NAMESPACES = {"OGC", "ESRI"}; + + /** + * Axis directions supported by PROJ4J. The PROJ4J code for each axis direction + * is the first letter of the name of code list value, converted to lower case. + */ + private static final Set SUPPORTED_AXIS_DIRECTIONS = new HashSet<>( + Arrays.asList(AxisDirection.NORTH, AxisDirection.SOUTH, + AxisDirection.EAST, AxisDirection.WEST, + AxisDirection.UP, AxisDirection.DOWN)); + + /** + * A registry for creating {@link Projection} instances if needed. + * If {@code null}, a default instance will be created when first needed. + * + * @see #getRegistry() + */ + protected Registry registry; + + /** + * Default instance used by {@code Wrappers.proj4j(…)} methods. + */ + static final Importer DEFAULT = new Importer(); + + /** + * Creates a default instance. + */ + public Importer() { + } + + /** + * Creates an importer which will use the given registry. + * + * @param registry a registry for creating {@link Projection} instances, or {@code null} for default + */ + public Importer(final Registry registry) { + this.registry = registry; + } + + /** + * {@return the registry to use for creating PROJ4J objects from a name}. + * If no registry was specified at construction time, a default instance + * is created the first time that this method is invoked. + */ + public synchronized Registry getRegistry() { + if (registry == null) { + registry = new Registry(); + } + return registry; + } + + /** + * Returns the given authority factory as a PROJ4J implementation. + * This method returns the backing implementation. + * If the given factory is not backed by a PROJ4J implementation, + * then the current implementation throws an exception. + * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public CRSFactory convert(final CRSAuthorityFactory src) { + if (src == null) { + return null; + } + if (src instanceof AuthorityFactoryWrapper) { + return ((AuthorityFactoryWrapper) src).impl; + } + throw new UnconvertibleInstanceException(getVendorName(src), "authority factory"); + } + + /** + * Returns the given CRS as a PROJ4J implementation. + * This method tries to return the backing implementation if possible, + * or otherwise copies the properties in a new PROJ4J instance. + * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public org.locationtech.proj4j.CoordinateReferenceSystem convert(final SingleCRS src) { + if (src == null) { + return null; + } + if (src instanceof AbstractCRS) { + return ((AbstractCRS) src).impl; + } + /* + * Try to map to the PROJ4J `Projection` object, including the parameter values. + * The `Projection` class is determined by the CRS type and the operation method. + */ + final GeodeticDatum frame; + final Projection projection; + if (src instanceof GeographicCRS) { + frame = ((GeographicCRS) src).getDatum(); + projection = new LongLatProjection(); + } else if (src instanceof ProjectedCRS) { + ProjectedCRS p = (ProjectedCRS) src; + frame = p.getDatum(); + projection = convert(p.getConversionFromBase().getParameterValues()); + } else { + throw new UnconvertibleInstanceException("The CRS must be geographic or projected."); + } + /* + * Set the `Projection` properties other than the parameters defined by the operation method. + * These properties are the CRS name, datum, ellipsoid, prime meridian and coordinate system. + * In the ISO 19111 model, these properties are in separated objects (not in the projection). + */ + final String name = getName(src); + projection.setName(name); + + final org.locationtech.proj4j.datum.Datum datum = convert(frame); + projection.setEllipsoid(datum.getEllipsoid()); + projection.setPrimeMeridian(convert(frame.getPrimeMeridian()).getName()); + + final CoordinateSystem cs = src.getCoordinateSystem(); + projection.setAxisOrder(axisOrder(cs)); // Checks the number of dimension as a side-effect. + final Unit unit = cs.getAxis(0).getUnit(); + if (!Objects.equals(unit, cs.getAxis(1).getUnit())) { + throw new UnconvertibleInstanceException("Heterogeneous unit of measurement."); + } else if (unit != null) { + projection.setUnits(Units.getInstance().proj4j(unit)); + } + return new org.locationtech.proj4j.CoordinateReferenceSystem(name, null, datum, projection); + } + + /** + * Returns the axis order of the given coordinate system. + * + * @param cs the coordinate system for which to get the axis order + * @return the 3-letters code of axis order to be given to {@link AxisOrder#fromString(String)}. + * @throws UnconvertibleInstanceException if the coordinate system uses an unsupported axis order + */ + static String axisOrder(final CoordinateSystem cs) { + final int dimension = cs.getDimension(); + if (dimension < Wrapper.BIDIMENSIONAL || dimension > Wrapper.TRIDIMENSIONAL) { + throw new UnconvertibleInstanceException("Unsupported " + dimension + " dimensional coordinate system."); + } + final char[] directions = new char[Wrapper.TRIDIMENSIONAL]; + directions[Wrapper.TRIDIMENSIONAL - 1] = 'u'; // Default value + for (int i=0; i) value).doubleValue(ac.getUnit())); + } catch (IllegalArgumentException | IllegalStateException | ClassCastException e) { + throw (UnconvertibleInstanceException) new UnconvertibleInstanceException( + "Cannot map \"" + name + "\" to a PROJ4J parameter.").initCause(e); + } + } + return proj; + } + + /** + * Returns the given coordinate operation factory as a PROJ4J implementation. + * This method returns the backing implementation. + * If the given factory is not backed by a PROJ4J implementation, + * then the current implementation throws an exception. + * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public CoordinateTransformFactory convert(final CoordinateOperationFactory src) { + if (src == null) { + return null; + } + if (src instanceof OperationFactoryWrapper) { + return ((OperationFactoryWrapper) src).impl; + } + throw new UnconvertibleInstanceException(getVendorName(src), "operation factory"); + } + + /** + * Returns the given coordinate operation as a PROJ4J implementation. + * This method returns the backing implementation. + * If the given factory is not backed by a PROJ4J implementation, + * then the current implementation throws an exception. + * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public CoordinateTransform convert(final CoordinateOperation src) { + if (src == null) { + return null; + } + if (src instanceof TransformWrapper) { + return ((TransformWrapper) src).impl; + } + throw new UnconvertibleInstanceException(getName(src), "coordinate operation"); + } + + /** + * Returns the name of the implementer of the given factory. + * This is used for error messages. + * + * @param factory the factory for which to get the implementer name + * @return name of the implementer of the given factory + */ + private static String getVendorName(final Factory factory) { + final Citation vendor = factory.getVendor(); + if (vendor != null) { + InternationalString title = vendor.getTitle(); + if (title != null) { + return title.toString(); + } + } + return factory.getClass().getSimpleName(); + } + + /** + * {@return the name of the given identified object}. This method is null-safe. + * Null safety is theoretically not necessary because the name is mandatory, but we try to be safe. + * + * @param src the object for which to get the name, or {@code null} + */ + private static String getName(final IdentifiedObject src) { + if (src != null) { + ReferenceIdentifier id = src.getName(); + if (id != null) { + return id.getCode(); + } + } + return null; + } + + /** + * Returns the first alias of the given identified object which is in the given scope. + * Aliases are often used for abbreviations. + * + * @param src the object for which to get an alias, or {@code null} + * @param scope scope of the alias to get, or {@code null} for the first alias regardless is scope + * @return the first alias, or {@code null} if none + */ + private static String getAlias(final IdentifiedObject src, final String scope) { + if (src != null) { + for (GenericName name : src.getAlias()) { + name = name.tip(); + if (scope == null) { + return name.toString(); + } + NameSpace ns = name.scope(); + if (ns != null && !ns.isGlobal() && scope.equalsIgnoreCase(ns.name().tip().toString())) { + return name.toString(); + } + } + } + return null; + } + + /** + * Returns the primary name or the first alias having one of the the given name spaces. + * If no name or alias is found, then the first non-null name or alias is returned. + * + * @param src the object for which to get a name or alias in the given name spaces + * @param scopes the desired name spaces, case-insensitive + * @return the first name in one of the given name space if any, or an arbitrary name otherwise + */ + private static String getNameOrAlias(final IdentifiedObject src, final String[] scopes) { + final ReferenceIdentifier name = src.getName(); + if (name != null) { + final String ns = name.getCodeSpace(); + for (String scope : scopes) { + if (scope.equalsIgnoreCase(ns)) { + return name.getCode(); + } + } + } + for (String scope : scopes) { + final String alias = getAlias(src, scope); + if (alias != null) { + return alias; + } + } + return (name != null) ? name.getCode() : getAlias(src, null); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationFactoryWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationFactoryWrapper.java new file mode 100644 index 0000000..34e817d --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationFactoryWrapper.java @@ -0,0 +1,161 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.util.Map; +import org.locationtech.proj4j.CoordinateTransformFactory; +import org.opengis.metadata.citation.Citation; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.crs.SingleCRS; +import org.opengis.referencing.operation.Conversion; +import org.opengis.referencing.operation.CoordinateOperation; +import org.opengis.referencing.operation.CoordinateOperationFactory; +import org.opengis.referencing.operation.OperationMethod; +import org.opengis.util.FactoryException; + + +/** + * Wraps a PROJ4J implementation behind the equivalent GeoAPI interface. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class OperationFactoryWrapper extends Wrapper implements CoordinateOperationFactory, Serializable { + /** + * The wrapped PROJ4 implementation. + */ + final CoordinateTransformFactory impl; + + /** + * Creates a new wrapper for the given PROJ4J implementation. + */ + private OperationFactoryWrapper(final CoordinateTransformFactory impl) { + this.impl = impl; + } + + /** + * Wraps the given implementation. + * + * @param impl the implementation to wrap, or {@code null} + * @return the wrapper, or {@code null} if the given implementation was null + */ + static OperationFactoryWrapper wrap(final CoordinateTransformFactory impl) { + return (impl != null) ? new OperationFactoryWrapper(impl) : null; + } + + /** + * {@return the PROJ4J backing implementation}. + */ + @Override + Object implementation() { + return impl; + } + + /** + * {@return the factory name}. + */ + @Override + public String getCode() { + return "PROJ4J"; + } + + /** + * {@return an identification of the softwware that provides the CRS definitions}. + * This is not the authority (EPSG, ESRI, etc). + */ + @Override + public Citation getVendor() { + return SimpleCitation.PROJ4J; + } + + /** + * Returns the given CRS as a PROJ4J implementation. This method avoids loading + * the {@link Importer} class when the given CRS is a PROJ4J wrapper. + * + * @param name "source" or "target", in case an error message needs to be produced + * @param crs the CRS to unwrap + * @return the PROJ4J object for the given CRS. + */ + private static org.locationtech.proj4j.CoordinateReferenceSystem unwrap( + final String name, final CoordinateReferenceSystem crs) + { + if (crs == null) { + throw new NullPointerException("The " + name + " CRS shall not be null."); + } + if (crs instanceof AbstractCRS) { + return ((AbstractCRS) crs).impl; + } else if (crs instanceof SingleCRS) { + return Importer.DEFAULT.convert((SingleCRS) crs); + } else { + throw new UnconvertibleInstanceException("The " + name + " CRS shall be a single CRS."); + } + } + + /** + * Creates a coordinate operation between the given pair of CRSs. + * + * @param sourceCRS the source coordinate reference system + * @param targetCRS the target coordinate reference system + * @return coordinate operation from source to target + * @throws FactoryException if the operation cannot be created + */ + @Override + public CoordinateOperation createOperation(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS) + throws FactoryException + { + // Unwrap first for checking null values and the number of dimensions (among others). + final org.locationtech.proj4j.CoordinateReferenceSystem src = unwrap("source", sourceCRS); + final org.locationtech.proj4j.CoordinateReferenceSystem tgt = unwrap("target", targetCRS); + final int srcDim = sourceCRS.getCoordinateSystem().getDimension(); + final int tgtDim = targetCRS.getCoordinateSystem().getDimension(); + if (srcDim != tgtDim) { + throw new FactoryException("Mismatched dimensions: source is " + srcDim + "D while target is " + tgtDim + "D."); + } + return TransformWrapper.wrap(impl.createTransform(src, tgt), srcDim >= TRIDIMENSIONAL); + } + + /** + * Creates a coordinate operation between the given pair of CRSs, ignoring the given method. + * + * @param sourceCRS the source coordinate reference system + * @param targetCRS the target coordinate reference system + * @param method ignored + * @return coordinate operation from source to target + * @throws FactoryException if the operation cannot be created + */ + @Override + public CoordinateOperation createOperation(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS, OperationMethod method) + throws FactoryException + { + return createOperation(sourceCRS, targetCRS); + } + + @Override + public CoordinateOperation createConcatenatedOperation(Map properties, CoordinateOperation... operations) + throws FactoryException + { + throw new FactoryException("Not implemented."); + } + + @Override + public Conversion createDefiningConversion(Map properties, OperationMethod method, ParameterValueGroup parameters) + throws FactoryException + { + throw new FactoryException("Not implemented."); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java index 8675b9f..7d81c0a 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/OperationMethodWrapper.java @@ -45,7 +45,7 @@ final class OperationMethodWrapper extends Wrapper implements OperationMethod, /** * The wrapped PROJ4 implementation. */ - private final Projection impl; + final Projection impl; /** * Creates a new wrapper for the given PROJ4J implementation. @@ -168,6 +168,8 @@ public ParameterDescriptorGroup getDescriptor() { /** * {@return the descriptions of all parameters having a non-default value}. + * The check for non-default values is a heuristic rule for identifying + * which parameters are used by the PROJ4J {@link Projection} instance. */ @Override public List descriptors() { @@ -176,6 +178,8 @@ public List descriptors() { /** * {@return the values of all parameters having a non-default value}. + * The check for non-default values is a heuristic rule for identifying + * which parameters are used by the PROJ4J {@link Projection} instance. */ @Override public List values() { diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java index f879490..833f34d 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/ParameterAccessor.java @@ -207,14 +207,19 @@ public Comparable getMaximumValue() { /** * {@return the minimum number of times that values for this parameter are required}. + * The value should be 1 for mandatory parameters and 0 for optional parameters. + * We consider all parameters as optional, because we don't know for sure which + * parameters are used by a particular PROJ4J {@link Projection} instance. */ @Override public int getMinimumOccurs() { - return 1; + return 0; } /** * {@return the maximum number of times that values for this parameter are required}. + * Values greater than 1 should happen only with parameter groups, which are not used + * in this implementation. */ @Override public int getMaximumOccurs() { diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java index 0c4edd7..a929b65 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PositionWrapper.java @@ -171,6 +171,9 @@ static void setLocation(final ProjCoordinate src, final DirectPosition tgt) { */ @SuppressWarnings("fallthrough") static ProjCoordinate unwrapOrCopy(final DirectPosition src) { + if (src == null) { + return null; + } if (src instanceof PositionWrapper) { return ((PositionWrapper) src).impl; } diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java index a9a5e48..b103cba 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/PrimeMeridianWrapper.java @@ -39,7 +39,7 @@ final class PrimeMeridianWrapper extends Wrapper implements PrimeMeridian, Seria /** * The wrapped PROJ4 implementation. */ - private final org.locationtech.proj4j.datum.PrimeMeridian impl; + final org.locationtech.proj4j.datum.PrimeMeridian impl; /** * Creates a new wrapper for the given PROJ4J implementation. diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/SimpleCitation.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/SimpleCitation.java new file mode 100644 index 0000000..2e0523e --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/SimpleCitation.java @@ -0,0 +1,270 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.io.Serializable; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import org.opengis.metadata.Identifier; +import org.opengis.metadata.citation.Address; +import org.opengis.metadata.citation.Citation; +import org.opengis.metadata.citation.CitationDate; +import org.opengis.metadata.citation.Contact; +import org.opengis.metadata.citation.OnLineFunction; +import org.opengis.metadata.citation.OnlineResource; +import org.opengis.metadata.citation.PresentationForm; +import org.opengis.metadata.citation.ResponsibleParty; +import org.opengis.metadata.citation.Role; +import org.opengis.metadata.citation.Series; +import org.opengis.metadata.citation.Telephone; +import org.opengis.util.InternationalString; + + +/** + * A citation containing only a title, an organization name and a URL. + * This implementation merges many interfaces in a single class for convenience. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +final class SimpleCitation implements Citation, ResponsibleParty, Contact, OnlineResource, Serializable { + /** + * The citation for the PROJ4J software. + */ + static final SimpleCitation PROJ4J = new SimpleCitation("PROJ4J", "Eclipse Foundation", + "LocationTech", "https://projects.eclipse.org/projects/locationtech"); + + /** + * The title of the dataset or project. + * + * @see #getTitle() + */ + private final String title; + + /** + * The organization responsible for the maintenance of the dataset or project. + * + * @see #getOrganisationName() + * @see #getCitedResponsibleParties() + */ + private final String organization; + + /** + * Name of the page referenced by {@link #url}. + */ + private final String urlName; + + /** + * URL where user can get more information. + */ + private final String url; + + /** + * Creates a new citation with the given title. + */ + private SimpleCitation(final String title, final String organization, final String urlName, final String url) { + this.title = title; + this.organization = organization; + this.urlName = urlName; + this.url = url; + } + + /** + * {@return the title of the dataset or project}. + * Examples: "EPSG", "PROJ4J". + */ + @Override + public InternationalString getTitle() { + return LocalizedString.wrap(title); + } + + /** + * {@return a description of how the dataset or project is presented}. + */ + @Override + public Collection getPresentationForms() { + return Collections.singleton(PresentationForm.valueOf("SOFTWARE")); + } + + /** + * {@return the organization together with other information such as the organization role}. + * This is the method invoked by users for accessing {@link #getOrganisationName()}. + */ + @Override + public Collection getCitedResponsibleParties() { + return Collections.singletonList(this); + } + + /** + * {@return the organization responsible for the maintenance of the dataset or project}. + * Examples: "IOGP", "Eclipse". + */ + @Override + public InternationalString getOrganisationName() { + return LocalizedString.wrap(organization); + } + + /** + * {@return the role of the organization regarding the software or data}. + */ + @Override + public Role getRole() { + return Role.OWNER; + } + + /** + * {@return information for contacting the responsible party}. + */ + @Override + public Contact getContactInfo() { + return this; + } + + /** + * {@return information about how to contact the organization}. + * Note that this is a member of contact information, not project information. + * + *

Note: for providing a link to the project instead of the organization, + * we need to wait for the release of GeoAPI 3.1.

+ */ + @Override + public OnlineResource getOnlineResource() { + return this; + } + + /** + * {@return name of the online resource}. It describes the content of {@link #getLinkage()}, + * which is about the organization, not the project. + */ + @Override + public String getName() { + return urlName; + } + + /** + * {@return URL to the organization web site}. + * Note that this is a member of contact information, not project information. + */ + @Override + public URI getLinkage() { + return URI.create(url); + } + + /** + * {@return the purpose of the linkage}. + */ + @Override + public OnLineFunction getFunction() { + return OnLineFunction.INFORMATION; + } + + @Override + public Collection getAlternateTitles() { + return Collections.emptyList(); + } + + @Override + public InternationalString getCollectiveTitle() { + return null; + } + + @Override + public Collection getDates() { + return Collections.emptyList(); + } + + @Override + public Date getEditionDate() { + return null; + } + + @Override + public InternationalString getEdition() { + return null; + } + + @Override + public Series getSeries() { + return null; + } + + @Override + public InternationalString getOtherCitationDetails() { + return null; + } + + @Override + public String getISBN() { + return null; + } + + @Override + public String getISSN() { + return null; + } + + @Override + public String getIndividualName() { + return null; + } + + @Override + public InternationalString getPositionName() { + return null; + } + + @Override + public Telephone getPhone() { + return null; + } + + @Override + public Address getAddress() { + return null; + } + + @Override + public InternationalString getHoursOfService() { + return null; + } + + @Override + public InternationalString getContactInstructions() { + return null; + } + + @Override + public String getProtocol() { + return null; + } + + @Override + public String getApplicationProfile() { + return null; + } + + @Override + public InternationalString getDescription() { + return null; + } + + @Override + public Collection getIdentifiers() { + return Collections.emptySet(); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/UnconvertibleInstanceException.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/UnconvertibleInstanceException.java new file mode 100644 index 0000000..6b957bc --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/UnconvertibleInstanceException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.locationtech.proj4j.Proj4jException; + + +/** + * Thrown by {@code Wrapper.proj4j(…)} when a GeoAPI object cannot be unwrapped or copied to a PROJ4J implementation. + * This exception is never thrown when the given GeoAPI object has been created by a {@code Wrapper.geoapi(…)} method. + * This exception may be thrown for GeoAPI objects created by other libraries, depending on the characteristics of the + * object. For example, it may depend on whether the coordinate system uses unsupported axis directions. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("serial") +public class UnconvertibleInstanceException extends Proj4jException { + /** + * Creates a new exception with no message. + */ + public UnconvertibleInstanceException() { + } + + /** + * Creates a new exception with the given message. + * + * @param message the exception message, or {@code null} + */ + public UnconvertibleInstanceException(String message) { + super(message); + } + + /** + * Creates a new exception with a message built for the given object name and type. + * + * @param name the name of the object that cannot be unwrapped + * @param type the type of the object that cannot be unwrapped + */ + UnconvertibleInstanceException(String name, String type) { + super("Cannot unwrap the \"" + name + "\" " + type + " as a PROJ4J implementation."); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java index bfd16f0..0405d08 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java @@ -98,4 +98,28 @@ private static Unit getDimensionless(final SystemOfUnits system) } return unit; } + + /** + * Returns the given JSR-363 unit of measurement as a PROJ4J instance. + * Note that there is no method in the reverse direction (from PROJ4J to JSR-363) + * because current PROJ4J does not tell us whether the unit is linear or angular. + * + * @param unit the unit of measurement + * @return the PROJ4J equivalent unit + * @throws UnconvertibleInstanceException if the unit cannot be mapped + */ + final org.locationtech.proj4j.units.Unit proj4j(final Unit unit) { + if (unit.equals(metre)) return org.locationtech.proj4j.units.Units.METRES; + if (unit.equals(degree)) return org.locationtech.proj4j.units.Units.DEGREES; + if (unit.equals(one)) return null; + + String symbol = unit.getSymbol().trim(); + if ("°".equals(symbol)) symbol = "degree"; + org.locationtech.proj4j.units.Unit proj4j = org.locationtech.proj4j.units.Units.findUnits(symbol); + if (org.locationtech.proj4j.units.Units.METRES.equals(proj4j)) { + // PROJ4J maps every unknown unit to metres, which is unsafe from GeoAPI point of view. + throw new UnconvertibleInstanceException("Cannot map \"" + symbol + "\" to PROJ4 unit."); + } + return proj4j; + } } diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java index 449112d..22b0b94 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Wrappers.java @@ -15,16 +15,24 @@ */ package org.locationtech.proj4j.geoapi; +import org.locationtech.proj4j.CRSFactory; import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.CoordinateTransformFactory; import org.locationtech.proj4j.ProjCoordinate; +import org.locationtech.proj4j.datum.AxisOrder; +import org.locationtech.proj4j.proj.Projection; import org.opengis.geometry.DirectPosition; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.crs.CRSAuthorityFactory; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.crs.SingleCRS; +import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.datum.GeodeticDatum; import org.opengis.referencing.datum.PrimeMeridian; import org.opengis.referencing.operation.CoordinateOperation; +import org.opengis.referencing.operation.CoordinateOperationFactory; /** @@ -39,6 +47,32 @@ public final class Wrappers { private Wrappers() { } + /** + * Wraps the given PROJ4J CRS factory behind the equivalent GeoAPI interface. + * The returned factory support only the creation of geographic and projected CRSs. + * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static CRSAuthorityFactory geoapi(final CRSFactory impl) { + return AuthorityFactoryWrapper.wrap(impl); + } + + /** + * Returns the given authority factory as a PROJ4J implementation. + * This method returns the backing implementation. + * + *

This is a convenience method for {@link Importer#convert(CRSAuthorityFactory)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static CRSFactory proj4j(final CRSAuthorityFactory src) { + return Importer.DEFAULT.convert(src); + } + /** * Wraps the given PROJ4J CRS behind the equivalent GeoAPI interface. * The returned object is a view: if any {@code impl} value is changed after this method call, @@ -57,6 +91,22 @@ public static SingleCRS geoapi(final org.locationtech.proj4j.CoordinateReference return AbstractCRS.wrap(impl, is3D); } + /** + * Returns the given CRS as a PROJ4J implementation. + * This method tries to return the backing implementation if possible, + * or otherwise copies the properties in a new PROJ4J instance. + * + *

This is a convenience method for {@link Importer#convert(SingleCRS)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static org.locationtech.proj4j.CoordinateReferenceSystem proj4j(final SingleCRS src) { + return Importer.DEFAULT.convert(src); + } + /** * Wraps the given PROJ4J datum behind the equivalent GeoAPI interface. * The returned object is a view: if any {@code impl} value is changed after this method call, @@ -70,6 +120,22 @@ public static GeodeticDatum geoapi(final org.locationtech.proj4j.datum.Datum imp return DatumWrapper.wrap(impl); } + /** + * Returns the given datum as a PROJ4J implementation. + * This method tries to return the backing implementation if possible, + * or otherwise copies the properties in a new PROJ4J instance. + * + *

This is a convenience method for {@link Importer#convert(GeodeticDatum)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static org.locationtech.proj4j.datum.Datum proj4j(final GeodeticDatum src) { + return Importer.DEFAULT.convert(src); + } + /** * Wraps the given PROJ4J ellipsoid behind the equivalent GeoAPI interface. * The returned object is a view: if any {@code impl} value is changed after this method call, @@ -83,6 +149,22 @@ public static Ellipsoid geoapi(final org.locationtech.proj4j.datum.Ellipsoid imp return EllipsoidWrapper.wrap(impl); } + /** + * Returns the given ellipsoid as a PROJ4J implementation. + * This method tries to return the backing implementation if possible, + * or otherwise copies the properties in a new PROJ4J instance. + * + *

This is a convenience method for {@link Importer#convert(Ellipsoid)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static org.locationtech.proj4j.datum.Ellipsoid proj4j(final Ellipsoid src) { + return Importer.DEFAULT.convert(src); + } + /** * Wraps the given PROJ4J ellipsoid behind the equivalent GeoAPI interface. * The returned object is a view: if any {@code impl} value is changed after this method call, @@ -96,6 +178,76 @@ public static PrimeMeridian geoapi(final org.locationtech.proj4j.datum.PrimeMeri return PrimeMeridianWrapper.wrap(impl); } + /** + * Returns the given prime meridian as a PROJ4J implementation. + * This method tries to return the backing implementation if possible, + * or an equivalent PROJ4J instance otherwise. + * + *

This is a convenience method for {@link Importer#convert(PrimeMeridian)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static org.locationtech.proj4j.datum.PrimeMeridian proj4j(final PrimeMeridian src) { + return Importer.DEFAULT.convert(src); + } + + /** + * Wraps the given PROJ4J projection behind the equivalent GeoAPI interface. + * The returned object is a view: if any {@code impl} value is changed after this method call, + * those changes will be reflected immediately in the returned view. The view is bidirectional: + * setting a value in the returned parameters modify a property of the given projection. + * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static ParameterValueGroup geoapi(final Projection impl) { + return OperationMethodWrapper.wrap(impl); + } + + /** + * Returns the given parameters as a PROJ4J implementation. + * This method tries to return the backing implementation if possible, + * or an equivalent PROJ4J instance otherwise. + * + *

This is a convenience method for {@link Importer#convert(ParameterValueGroup)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static Projection proj4j(final ParameterValueGroup src) { + return Importer.DEFAULT.convert(src); + } + + /** + * Wraps the given PROJ4J coordinate operation factory behind the equivalent GeoAPI interface. + * + * @param impl the implementation to wrap, or {@code null} + * @return the view, or {@code null} if the given implementation was null + */ + public static CoordinateOperationFactory geoapi(final CoordinateTransformFactory impl) { + return OperationFactoryWrapper.wrap(impl); + } + + /** + * Returns the given coordinate operation factory as a PROJ4J implementation. + * This method returns the backing implementation. + * + *

This is a convenience method for {@link Importer#convert(CoordinateOperationFactory)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static CoordinateTransformFactory proj4j(final CoordinateOperationFactory src) { + return Importer.DEFAULT.convert(src); + } + /** * Wraps the given PROJ4J coordinate transform behind the equivalent GeoAPI interface. * The returned object is a view: if any {@code impl} value is changed after this method call, @@ -110,6 +262,21 @@ public static CoordinateOperation geoapi(final CoordinateTransform impl, final b return TransformWrapper.wrap(impl, is3D); } + /** + * Returns the given coordinate operation as a PROJ4J implementation. + * This method returns the backing implementation. + * + *

This is a convenience method for {@link Importer#convert(CoordinateOperation)} + * on a default instance of {@code Importer}.

+ * + * @param src the object to unwrap or convert, or {@code null} + * @return the PROJ4J implementation, or {@code null} if the given object was null + * @throws UnconvertibleInstanceException if the given object cannot be unwrapped or converted + */ + public static CoordinateTransform proj4j(final CoordinateOperation src) { + return Importer.DEFAULT.convert(src); + } + /** * Wraps the given PROJ4J coordinate tuple behind the equivalent GeoAPI interface. * The returned object is a view: if any {@code impl} value is changed after this method call, @@ -122,4 +289,27 @@ public static CoordinateOperation geoapi(final CoordinateTransform impl, final b public static DirectPosition geoapi(final ProjCoordinate impl) { return PositionWrapper.wrap(impl); } + + /** + * Returns the given position as a PROJ4J coordinate tuple. + * This method tries to return the backing implementation if possible, + * or otherwise copies the coordinate values in a new coordinate tuple. + * + * @param src the position to unwrap or convert, or {@code null} + * @return the coordinates, or {@code null} if the given object was null + */ + public static ProjCoordinate proj4j(final DirectPosition src) { + return PositionWrapper.unwrapOrCopy(src); + } + + /** + * Returns the axis order of the given coordinate system. + * + * @param cs the coordinate system for which to get the axis order, or {@code null} + * @return the axis order, or {@code null} if the given coordinate system was null + * @throws UnconvertibleInstanceException if the coordinate system uses an unsupported axis order + */ + public static AxisOrder axisOrder(final CoordinateSystem cs) { + return (cs != null) ? AxisOrder.fromString(Importer.axisOrder(cs)) : null; + } } diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java index e8fc70d..864e462 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java @@ -20,6 +20,15 @@ * with overloaded {@code geoapi(…)} methods. Those methods expected a PROJ4J object in * argument and returns a view of that object as a GeoAPI type. * + *

Dependency to a Unit of Measurement library

+ * This module requires a JSR-363 (Units of Measurement) implementation + * to be present on the class-path or module-path. + * The choice of an implementation is left to the user. Some implementations are + * Indriya, + * Seshat and + * Apache SIS. + * The two latter support EPSG codes for units of measurement. + * *

Mutability

* No information is copied. All methods of the views delegate their work to the PROJ4J implementation. * Consequently, since PROJ4J objects are mutable, changes to the wrapped PROJ4J object are immediately @@ -30,6 +39,12 @@ * Because the type of a Java object cannot change dynamically, whether a CRS is geographic or projected * is determined at {@code geoapi(CoordinateReferenceSystem)} invocation time.

* + *

Serialization

+ * The serialization details are not committed API. + * Serialization is okay for exchanging objects between JVM running the same version of PROJ4J, + * but is not guaranteed to be compatible between different versions of PROJ4J. This module does not define + * {@code serialVersionUID} because the backing PROJ4J objects do not define those UID anyway. + * * @author Martin Desruisseaux (Geomatys) */ package org.locationtech.proj4j.geoapi; diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java new file mode 100644 index 0000000..64f42ea --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java @@ -0,0 +1,135 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi.spi; + +import java.util.Set; +import org.locationtech.proj4j.CRSFactory; +import org.locationtech.proj4j.geoapi.Wrappers; +import org.opengis.metadata.citation.Citation; +import org.opengis.referencing.IdentifiedObject; +import org.opengis.referencing.crs.*; +import org.opengis.util.FactoryException; +import org.opengis.util.InternationalString; + + +/** + * Registers PROJ4J wrappers as a CRS authority factory. + * + *

Future evolution

+ * In a future version, it may not be possible anymore to instantiate this class. + * For now, we have to allow instantiation for compatibility with Java 8 services. + * If a future version of PROJ4J migrates to Java 9 module system, the only way to + * get the factory will by invoking the {@link #provider()} static method. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class AuthorityFactory implements CRSAuthorityFactory { + /** + * Where to delegate all operations. + */ + private final CRSAuthorityFactory proxy; + + /** + * Creates a new instance. + * WARNING: this constructor may not be accessible anymore in a future version. + * Do not invoke directly. + */ + public AuthorityFactory() { + proxy = provider(); + } + + /** + * {@return the factory backed by PROJ4J}. + */ + public static CRSAuthorityFactory provider() { + return Wrappers.geoapi(new CRSFactory()); + } + + @Override + public Citation getVendor() { + return proxy.getVendor(); + } + + @Override + public CoordinateReferenceSystem createCoordinateReferenceSystem(String code) throws FactoryException { + return proxy.createCoordinateReferenceSystem(code); + } + + @Override + public CompoundCRS createCompoundCRS(String code) throws FactoryException { + return proxy.createCompoundCRS(code); + } + + @Override + public DerivedCRS createDerivedCRS(String code) throws FactoryException { + return proxy.createDerivedCRS(code); + } + + @Override + public EngineeringCRS createEngineeringCRS(String code) throws FactoryException { + return proxy.createEngineeringCRS(code); + } + + @Override + public GeographicCRS createGeographicCRS(String code) throws FactoryException { + return proxy.createGeographicCRS(code); + } + + @Override + public GeocentricCRS createGeocentricCRS(String code) throws FactoryException { + return proxy.createGeocentricCRS(code); + } + + @Override + public ImageCRS createImageCRS(String code) throws FactoryException { + return proxy.createImageCRS(code); + } + + @Override + public ProjectedCRS createProjectedCRS(String code) throws FactoryException { + return proxy.createProjectedCRS(code); + } + + @Override + public TemporalCRS createTemporalCRS(String code) throws FactoryException { + return proxy.createTemporalCRS(code); + } + + @Override + public VerticalCRS createVerticalCRS(String code) throws FactoryException { + return proxy.createVerticalCRS(code); + } + + @Override + public Citation getAuthority() { + return proxy.getAuthority(); + } + + @Override + public Set getAuthorityCodes(Class type) throws FactoryException { + return proxy.getAuthorityCodes(type); + } + + @Override + public InternationalString getDescriptionText(String code) throws FactoryException { + return proxy.getDescriptionText(code); + } + + @Override + public IdentifiedObject createObject(String code) throws FactoryException { + return proxy.createObject(code); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java new file mode 100644 index 0000000..2e4fc30 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi.spi; + +import java.util.Map; +import org.locationtech.proj4j.CoordinateTransformFactory; +import org.locationtech.proj4j.geoapi.Wrappers; +import org.opengis.metadata.citation.Citation; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.*; +import org.opengis.util.FactoryException; + + +/** + * Registers PROJ4J wrappers as an operation factory. + * + *

Future evolution

+ * In a future version, it may not be possible anymore to instantiate this class. + * For now, we have to allow instantiation for compatibility with Java 8 services. + * If a future version of PROJ4J migrates to Java 9 module system, the only way to + * get the factory will by invoking the {@link #provider()} static method. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class OperationFactory implements CoordinateOperationFactory { + /** + * Where to delegate all operations. + */ + private final CoordinateOperationFactory proxy; + + /** + * Creates a new instance. + * WARNING: this constructor may not be accessible anymore in a future version. + * Do not invoke directly. + */ + public OperationFactory() { + proxy = provider(); + } + + /** + * {@return the factory backed by PROJ4J}. + */ + public static CoordinateOperationFactory provider() { + return Wrappers.geoapi(new CoordinateTransformFactory()); + } + + @Override + public Citation getVendor() { + return proxy.getVendor(); + } + + @Override + public CoordinateOperation createOperation(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS) throws FactoryException { + return proxy.createOperation(sourceCRS, targetCRS); + } + + @Override + public CoordinateOperation createOperation(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS, OperationMethod method) throws FactoryException { + return proxy.createOperation(sourceCRS, targetCRS, method); + } + + @Override + public CoordinateOperation createConcatenatedOperation(Map properties, CoordinateOperation... operations) throws FactoryException { + return proxy.createConcatenatedOperation(properties, operations); + } + + @Override + public Conversion createDefiningConversion(Map properties, OperationMethod method, ParameterValueGroup parameters) throws FactoryException { + return proxy.createDefiningConversion(properties, method, parameters); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/package-info.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/package-info.java new file mode 100644 index 0000000..f54cff7 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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. + */ + +/** + * Registration of PROJ4J wrappers as referencing services. + * Developers should not use this package directly, as it may change in any future version. + * In particular, it may be simplified if PROJ4J migrates to Java 9+ module system. + * + * @author Martin Desruisseaux (Geomatys) + */ +package org.locationtech.proj4j.geoapi.spi; diff --git a/geoapi/src/main/resources/META-INF/services/org.opengis.referencing.crs.CRSAuthorityFactory b/geoapi/src/main/resources/META-INF/services/org.opengis.referencing.crs.CRSAuthorityFactory new file mode 100644 index 0000000..ea6f44b --- /dev/null +++ b/geoapi/src/main/resources/META-INF/services/org.opengis.referencing.crs.CRSAuthorityFactory @@ -0,0 +1 @@ +org.locationtech.proj4j.geoapi.spi.AuthorityFactory diff --git a/geoapi/src/main/resources/META-INF/services/org.opengis.referencing.operation.CoordinateOperationFactory b/geoapi/src/main/resources/META-INF/services/org.opengis.referencing.operation.CoordinateOperationFactory new file mode 100644 index 0000000..dca2fb5 --- /dev/null +++ b/geoapi/src/main/resources/META-INF/services/org.opengis.referencing.operation.CoordinateOperationFactory @@ -0,0 +1 @@ +org.locationtech.proj4j.geoapi.spi.OperationFactory diff --git a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/ServicesTest.java b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/ServicesTest.java new file mode 100644 index 0000000..1d6f0a8 --- /dev/null +++ b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/ServicesTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import java.util.Iterator; +import java.util.ServiceLoader; +import org.junit.Test; +import org.opengis.referencing.crs.CRSAuthorityFactory; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.CoordinateOperationFactory; +import org.opengis.util.FactoryException; + +import static org.locationtech.proj4j.CoordinateReferenceSystem.CS_GEO; +import static org.junit.Assert.*; + + +/** + * Tests fetching factory instances as services. + * + * @author Martin Desruisseaux (Geomatys) + */ +public class ServicesTest { + /** + * Creates a new test case. + */ + public ServicesTest() { + } + + /** + * Returns the factory of the given type, making sure that there is exactly one instance. + */ + private static F getSingleton(final Class service) { + Iterator it = ServiceLoader.load(service).iterator(); + assertTrue(it.hasNext()); + F factory = it.next(); + assertFalse(it.hasNext()); + return factory; + } + + /** + * Tests the CRS authority factory. + * This method only checks that the object are non-null. + * More detailed checks are performed by {@link WrappersTest}. + * + * @throws FactoryException if an error occurred while creating an object. + */ + @Test + public void testAuthorityFactory() throws FactoryException { + final CRSAuthorityFactory factory = getSingleton(CRSAuthorityFactory.class); + assertNotNull(factory.createGeographicCRS("EPSG:4326")); + assertNotNull(factory.createProjectedCRS ("EPSG:2154")); + } + + /** + * Tests the operation authority factory. + * This method only checks that the object are non-null. + * + * @throws FactoryException if an error occurred while creating an object. + */ + @Test + public void testOperationFactory() throws FactoryException { + final CoordinateOperationFactory factory = getSingleton(CoordinateOperationFactory.class); + final CoordinateReferenceSystem crs = Wrappers.geoapi(CS_GEO, false); + assertTrue(factory.createOperation(crs, crs).getMathTransform().isIdentity()); + } +} diff --git a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java index 03b1d85..6d420f6 100644 --- a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java +++ b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java @@ -24,6 +24,7 @@ import org.opengis.geometry.DirectPosition; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; +import org.opengis.referencing.crs.CRSAuthorityFactory; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.datum.GeodeticDatum; @@ -36,11 +37,11 @@ import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.OperationMethod; import org.opengis.referencing.operation.Projection; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; import org.opengis.referencing.operation.TransformException; +import org.locationtech.proj4j.datum.AxisOrder; +import org.opengis.util.FactoryException; + +import static org.junit.Assert.*; /** @@ -55,6 +56,13 @@ public final class WrappersTest { public WrappersTest() { } + /** + * {@return a factory to use for testing CRS creation}. + */ + private static CRSAuthorityFactory crsFactory() { + return Wrappers.geoapi(new CRSFactory()); + } + /** * Tests the wrapping of a datum. */ @@ -75,12 +83,13 @@ public void testDatum() { /** * Tests the creation of a geographic CRS. * This method verifies the datum (including its dependencies) and the coordinate system. + * + * @throws FactoryException if the CRS cannot be created */ @Test - public void testGeographicCRS() { + public void testGeographicCRS() throws FactoryException { final Unit degree = Units.getInstance().degree; - final CRSFactory crsFactory = new CRSFactory(); - final GeographicCRS crs = (GeographicCRS) Wrappers.geoapi(crsFactory.createFromName("EPSG:4326"), false); + final GeographicCRS crs = crsFactory().createGeographicCRS("EPSG:4326"); assertEquals("EPSG:4326", crs.getName().getCode()); /* @@ -106,6 +115,7 @@ public void testGeographicCRS() { * Verify axis name, abbreviation, direction and unit. */ final EllipsoidalCS cs = crs.getCoordinateSystem(); + assertEquals(AxisOrder.ENU, Wrappers.axisOrder(cs)); assertEquals(2, cs.getDimension()); CoordinateSystemAxis axis = cs.getAxis(0); @@ -128,14 +138,14 @@ public void testGeographicCRS() { * This method verifies the datum, the coordinate system and the projection parameters. * Opportunistically tests the transformation of a point. * + * @throws FactoryException if the CRS cannot be created * @throws TransformException if an error occurred while testing the projection of a point */ @Test - public void testProjectedCRS() throws TransformException { + public void testProjectedCRS() throws FactoryException, TransformException { final Unit degree = Units.getInstance().degree; final Unit metre = Units.getInstance().metre; - final CRSFactory crsFactory = new CRSFactory(); - final ProjectedCRS crs = (ProjectedCRS) Wrappers.geoapi(crsFactory.createFromName("EPSG:2154"), false); + final ProjectedCRS crs = crsFactory().createProjectedCRS("EPSG:2154"); assertEquals("EPSG:2154", crs.getName().getCode()); /* @@ -159,6 +169,7 @@ public void testProjectedCRS() throws TransformException { * Verify axis name, abbreviation, direction and unit. */ final CartesianCS cs = crs.getCoordinateSystem(); + assertEquals(AxisOrder.ENU, Wrappers.axisOrder(cs)); assertEquals(2, cs.getDimension()); CoordinateSystemAxis axis = cs.getAxis(0); From 2ae120724ddaa9291575e9168a6d65fc2af586b5 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Tue, 11 Mar 2025 17:08:27 +0100 Subject: [PATCH 6/7] Add a few tests and validations provided by the by GeoAPI conformance module. --- geoapi/pom.xml | 6 ++ .../org/locationtech/proj4j/geoapi/Alias.java | 25 +++++- .../locationtech/proj4j/geoapi/Services.java | 85 +++++++++++++++++++ .../proj4j/geoapi/TransformWrapper2D.java | 10 ++- .../proj4j/geoapi/TransformWrapper3D.java | 10 ++- .../proj4j/geoapi/spi/AuthorityFactory.java | 8 +- .../proj4j/geoapi/spi/OperationFactory.java | 8 +- .../proj4j/geoapi/TransformTest.java | 75 ++++++++++++++++ .../proj4j/geoapi/WrappersTest.java | 13 ++- 9 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 geoapi/src/main/java/org/locationtech/proj4j/geoapi/Services.java create mode 100644 geoapi/src/test/java/org/locationtech/proj4j/geoapi/TransformTest.java diff --git a/geoapi/pom.xml b/geoapi/pom.xml index f458c16..e722707 100644 --- a/geoapi/pom.xml +++ b/geoapi/pom.xml @@ -27,6 +27,12 @@ geoapi 3.0.2 + + org.opengis + geoapi-conformance + 3.0.2 + test + org.locationtech.proj4j proj4j diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Alias.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Alias.java index 592f2b6..c0a17e9 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Alias.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Alias.java @@ -34,7 +34,12 @@ * @author Martin Desruisseaux (Geomatys) */ @SuppressWarnings("serial") -final class Alias implements LocalName, Serializable { +final class Alias implements LocalName, NameSpace, Serializable { + /** + * Name of the global name space. + */ + private static final Alias GLOBAL = new Alias("global"); + /** * The name to provide as an alias. */ @@ -59,9 +64,12 @@ static Collection wrap(final String name) { return (name != null) ? Collections.singletonList(new Alias(name)) : Collections.emptyList(); } + /** + * {@return the global namespace}. + */ @Override public NameSpace scope() { - return null; + return this; } @Override @@ -122,4 +130,17 @@ public boolean equals(Object o) { public int hashCode() { return name.hashCode() ^ getClass().hashCode(); } + + @Override + public boolean isGlobal() { + return true; + } + + /** + * {@return the name of the global name space}. + */ + @Override + public GenericName name() { + return GLOBAL; + } } diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Services.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Services.java new file mode 100644 index 0000000..cc78673 --- /dev/null +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Services.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.locationtech.proj4j.geoapi.spi.AuthorityFactory; +import org.locationtech.proj4j.geoapi.spi.OperationFactory; +import org.opengis.referencing.NoSuchAuthorityCodeException; +import org.opengis.referencing.crs.CRSAuthorityFactory; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.CoordinateOperation; +import org.opengis.referencing.operation.CoordinateOperationFactory; +import org.opengis.util.FactoryException; + + +/** + * Default implementations of referencing services backed by PROJ4J. + * Those services are accessible by {@link java.util.ServiceLoader}, + * which should be used by implementation-neutral applications. + * This class provides shortcuts for the convenience of applications + * that do not need implementation neutrality. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class Services { + /** + * Do not allows instantiation of this class. + */ + private Services() { + } + + /** + * {@return the singleton factory for creating CRS from authority codes}. + */ + public static CRSAuthorityFactory getAuthorityFactory() { + return AuthorityFactory.provider(); + } + + /** + * {@return the singleton factory for creating coordinate operations between a pair of CRS}. + */ + public static CoordinateOperationFactory getOperationFactory() { + return OperationFactory.provider(); + } + + /** + * Creates a coordinate reference system from the given authority code. + * The argument should be of the form {@code "AUTHORITY:CODE"}. + * If the authority is unspecified, then {@code "EPSG"} is assumed. + * + * @param code the authority code + * @return coordinate reference system for the given code + * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found + * @throws FactoryException if the object creation failed for some other reason + */ + public static CoordinateReferenceSystem createCRS(final String code) throws FactoryException { + return getAuthorityFactory().createCoordinateReferenceSystem(code); + } + + /** + * Creates a coordinate operation between the given pair of coordinate reference systems. + * + * @param source input coordinate reference system + * @param target output coordinate reference system + * @return a coordinate operation from {@code source} to {@code target} + * @throws FactoryException if the coordinate operation cannot be created + */ + public static CoordinateOperation findOperation(CoordinateReferenceSystem source, CoordinateReferenceSystem target) + throws FactoryException + { + return getOperationFactory().createOperation(source, target); + } +} diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java index c9b99d7..8537d04 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper2D.java @@ -100,9 +100,10 @@ public void transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) throws TransformException { checkNumPts(numPts); - if (srcPts == dstPts && srcOff > dstOff) { + if (srcPts == dstPts && srcOff < dstOff) { + // If there is an overlap, we need a copy. int end = srcOff + numPts * BIDIMENSIONAL; - if (end < dstOff) { + if (end > dstOff) { srcPts = Arrays.copyOfRange(srcPts, srcOff, end); srcOff = 0; } @@ -132,9 +133,10 @@ public void transform(float[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) throws TransformException { checkNumPts(numPts); - if (srcPts == dstPts && srcOff > dstOff) { + if (srcPts == dstPts && srcOff < dstOff) { + // If there is an overlap, we need a copy. int end = srcOff + numPts * BIDIMENSIONAL; - if (end < dstOff) { + if (end > dstOff) { srcPts = Arrays.copyOfRange(srcPts, srcOff, end); srcOff = 0; } diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java index 3281fd5..c8a9e7d 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/TransformWrapper3D.java @@ -71,9 +71,10 @@ public void transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) throws TransformException { checkNumPts(numPts); - if (srcPts == dstPts && srcOff > dstOff) { + if (srcPts == dstPts && srcOff < dstOff) { + // If there is an overlap, we need a copy. int end = srcOff + numPts * TRIDIMENSIONAL; - if (end < dstOff) { + if (end > dstOff) { srcPts = Arrays.copyOfRange(srcPts, srcOff, end); srcOff = 0; } @@ -105,9 +106,10 @@ public void transform(float[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) throws TransformException { checkNumPts(numPts); - if (srcPts == dstPts && srcOff > dstOff) { + if (srcPts == dstPts && srcOff < dstOff) { + // If there is an overlap, we need a copy. int end = srcOff + numPts * TRIDIMENSIONAL; - if (end < dstOff) { + if (end > dstOff) { srcPts = Arrays.copyOfRange(srcPts, srcOff, end); srcOff = 0; } diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java index 64f42ea..cd40af0 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/AuthorityFactory.java @@ -37,6 +37,11 @@ * @author Martin Desruisseaux (Geomatys) */ public final class AuthorityFactory implements CRSAuthorityFactory { + /** + * The unique instance returned by {@link #provider()}. + */ + private static final CRSAuthorityFactory INSTANCE = Wrappers.geoapi(new CRSFactory()); + /** * Where to delegate all operations. */ @@ -53,9 +58,10 @@ public AuthorityFactory() { /** * {@return the factory backed by PROJ4J}. + * This is the method that should be invoked when using Java 9+ module system. */ public static CRSAuthorityFactory provider() { - return Wrappers.geoapi(new CRSFactory()); + return INSTANCE; } @Override diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java index 2e4fc30..133db2b 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/spi/OperationFactory.java @@ -37,6 +37,11 @@ * @author Martin Desruisseaux (Geomatys) */ public final class OperationFactory implements CoordinateOperationFactory { + /** + * The unique instance returned by {@link #provider()}. + */ + private static final CoordinateOperationFactory INSTANCE = Wrappers.geoapi(new CoordinateTransformFactory()); + /** * Where to delegate all operations. */ @@ -53,9 +58,10 @@ public OperationFactory() { /** * {@return the factory backed by PROJ4J}. + * This is the method that should be invoked when using Java 9+ module system. */ public static CoordinateOperationFactory provider() { - return Wrappers.geoapi(new CoordinateTransformFactory()); + return INSTANCE; } @Override diff --git a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/TransformTest.java b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/TransformTest.java new file mode 100644 index 0000000..768ceac --- /dev/null +++ b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/TransformTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2025, PROJ4J contributors + * + * Licensed 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.locationtech.proj4j.geoapi; + +import org.junit.Test; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.TransformException; +import org.opengis.test.referencing.TransformTestCase; +import org.opengis.util.FactoryException; + + +/** + * Tests some coordinate operations. + * + * @author Martin Desruisseaux (Geomatys) + */ +public class TransformTest extends TransformTestCase { + /** + * Creates a new test case. + */ + public TransformTest() { + } + + /** + * Creates a transform between the given pair of coordinate reference systems. + * + * @param source authority code of the input coordinate reference system + * @param target authority code of the output coordinate reference system + * @return a coordinate operation from {@code source} to {@code target} + * @throws FactoryException if the coordinate operation cannot be created + */ + private static MathTransform transform(String source, String target) throws FactoryException { + return Services.findOperation(Services.createCRS(source), Services.createCRS(target)).getMathTransform(); + } + + /** + * Tests a projection from a geographic CRS to a projected CRS. + * + * @throws FactoryException if a CRS cannot be created + * @throws TransformException if an error occurred while testing the projection of a point + */ + @Test + public void testProjection() throws FactoryException, TransformException { + transform = transform("EPSG:4326", "EPSG:2154"); + tolerance = 1E-3; + verifyTransform(new double[] {3, 46.5}, // Coordinates to test (more can be added on this line). + new double[] {700000, 6600000}); // Expected result. + + // Random coordinates. + final float[] coordinates = { + 3.0f, 46.5f, + 2.5f, 43.0f, + 3.5f, 46.0f, + 4.5f, 48.0f, + 1.5f, 41.0f, + 3.8f, 43.7f, + 3.1f, 42.1f, + }; + verifyConsistency(coordinates); + verifyInverse(coordinates); + } +} diff --git a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java index 6d420f6..656a95f 100644 --- a/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java +++ b/geoapi/src/test/java/org/locationtech/proj4j/geoapi/WrappersTest.java @@ -21,6 +21,7 @@ import org.junit.Test; import org.locationtech.proj4j.CRSFactory; import org.locationtech.proj4j.ProjCoordinate; +import org.locationtech.proj4j.datum.AxisOrder; import org.opengis.geometry.DirectPosition; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; @@ -38,8 +39,8 @@ import org.opengis.referencing.operation.OperationMethod; import org.opengis.referencing.operation.Projection; import org.opengis.referencing.operation.TransformException; -import org.locationtech.proj4j.datum.AxisOrder; import org.opengis.util.FactoryException; +import org.opengis.test.Validators; import static org.junit.Assert.*; @@ -78,6 +79,9 @@ public void testDatum() { assertEquals(6378388, ellipsoid.getSemiMajorAxis(), 0); assertEquals(6356911.95, ellipsoid.getSemiMinorAxis(), 0.005); assertEquals(297, ellipsoid.getInverseFlattening(), 5E-10); + + // Verification by GeoAPI + Validators.validate(datum); } /** @@ -131,6 +135,9 @@ public void testGeographicCRS() throws FactoryException { assertEquals(AxisDirection.NORTH, axis.getDirection()); assertEquals(degree, axis.getUnit()); assertSame(axis, cs.getAxis(1)); + + // Verification by GeoAPI + Validators.validate(crs); } /** @@ -231,5 +238,9 @@ public void testProjectedCRS() throws FactoryException, TransformException { assertEquals(2, pt.getDimension()); assertEquals(46.5, pt.getOrdinate(1), 1E-9); assertEquals( 3.0, pt.getOrdinate(0), 1E-9); + + // Verification by GeoAPI + // Disabled because one of the test is a bit too strict. This is fixed in GeoAPI 3.1. + // Validators.validate(crs); } } From 2e8a19d77935bb44ae3ec847d15c93cb8a46e377 Mon Sep 17 00:00:00 2001 From: Martin Desruisseaux Date: Tue, 11 Mar 2025 18:23:21 +0100 Subject: [PATCH 7/7] Build with Java 21 with the Java release fixed to 8. Java version has been verified with `javap`. --- .github/workflows/ci.yaml | 4 ++-- .../src/main/java/org/locationtech/proj4j/geoapi/Units.java | 6 +++--- .../java/org/locationtech/proj4j/geoapi/package-info.java | 2 +- pom.xml | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d4fef12..7724a2a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 1.8 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'adopt' - java-version: '8' + java-version: '21' - uses: actions/cache@v4 with: path: ~/.m2/repository diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java index 0405d08..7a67fdd 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/Units.java @@ -25,7 +25,7 @@ /** * Predefined constants for the units of measurement. - * The actual JSR-363 implementation is left at user's choice. + * The actual JSR-385 implementation is left at user's choice. * * @author Martin Desruisseaux (Geomatys) */ @@ -100,8 +100,8 @@ private static Unit getDimensionless(final SystemOfUnits system) } /** - * Returns the given JSR-363 unit of measurement as a PROJ4J instance. - * Note that there is no method in the reverse direction (from PROJ4J to JSR-363) + * Returns the given JSR-385 unit of measurement as a PROJ4J instance. + * Note that there is no method in the reverse direction (from PROJ4J to JSR-385) * because current PROJ4J does not tell us whether the unit is linear or angular. * * @param unit the unit of measurement diff --git a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java index 864e462..c406d7a 100644 --- a/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java +++ b/geoapi/src/main/java/org/locationtech/proj4j/geoapi/package-info.java @@ -21,7 +21,7 @@ * argument and returns a view of that object as a GeoAPI type. * *

Dependency to a Unit of Measurement library

- * This module requires a JSR-363 (Units of Measurement) implementation + * This module requires a JSR-385 (Units of Measurement) implementation * to be present on the class-path or module-path. * The choice of an implementation is left to the user. Some implementations are * Indriya, diff --git a/pom.xml b/pom.xml index aa59608..ef34108 100644 --- a/pom.xml +++ b/pom.xml @@ -70,8 +70,7 @@ maven-compiler-plugin 3.11.0 - 1.8 - 1.8 + 8 true UTF-8