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:
truetrue
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.proj4jproj4j
- 1.3.1-SNAPSHOTbundleProj4J
- https://github.com/locationtech/proj4jJava 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-8org.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.felixmaven-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.proj4jproj4j-epsg
- 1.3.1-SNAPSHOTjarProj4J EPSG
- https://github.com/locationtech/proj4jJava 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-SNAPSHOTpom
@@ -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
+
+
+
+
+
+
+
+
+
coreepsg
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 @@
junitjunit
- 4.13.2test
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 @@
coreepsg
+ 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 extends IdentifiedObject> 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 extends IdentifiedObject> 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 @@
geoapi3.0.2
+
+ org.opengis
+ geoapi-conformance
+ 3.0.2
+ test
+ org.locationtech.proj4jproj4j
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-plugin3.11.0
- 1.8
- 1.8
+ 8trueUTF-8