This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch feat/exportableTransform
in repository https://gitbox.apache.org/repos/asf/sis.git

commit cb366ebe13514f006ae59c6446e0c2ec01dc67a5
Author: jsorel <[email protected]>
AuthorDate: Tue Nov 5 15:41:42 2024 +0100

    Add ExportableTransform interface
---
 .../sis/referencing/ExportableTransform.java       |  55 ++++++++
 .../operation/projection/ConformalProjection.java  |   8 ++
 .../projection/EquidistantCylindrical.java         |  32 +++++
 .../projection/LambertConicConformal.java          |  85 +++++++++++++
 .../operation/projection/LongitudeWraparound.java  |  15 ++-
 .../referencing/operation/projection/Mercator.java |  81 ++++++++++++
 .../operation/projection/NormalizedProjection.java |  19 ++-
 .../operation/projection/PolarStereographic.java   |  86 +++++++++++++
 .../operation/projection/TransverseMercator.java   | 138 ++++++++++++++++++++-
 .../operation/transform/ConcatenatedTransform.java |  36 +++++-
 .../transform/EllipsoidToCentricTransform.java     | 101 ++++++++++++++-
 .../operation/transform/LinearTransform.java       |  53 +++++++-
 .../operation/transform/PassThroughTransform.java  |  45 ++++++-
 13 files changed, 744 insertions(+), 10 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/ExportableTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/ExportableTransform.java
new file mode 100644
index 0000000000..52f9238575
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/ExportableTransform.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing;
+
+/**
+ * Experimental API to export transforms to other syntaxes.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public interface ExportableTransform {
+
+    /**
+     * The produced code should transform one value only.
+     * Input array is expected to be named src.
+     * Ouput array is expected to be named dat.
+     *
+     * Example of expected produced code :
+     * <pre>
+     * {@code
+     * {
+     *   _CONSTANT_1: 123.456,
+     *   _CONSTANT_2: 234.567,
+     *
+     *   transform(src) {
+     *     //complex math using _CONSTANT_1 and _internalFunction1
+     *     return dst;
+     *   },
+     *
+     *   _internalFunction1(val1, val2) {
+     *     //does something
+     *   }
+     * }
+     * }
+     * </pre>
+     *
+     *
+     * @return ECMAScript code
+     * @throws UnsupportedOperationException if export is not possible or not 
available
+     */
+    String toECMAScript() throws UnsupportedOperationException;
+}
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ConformalProjection.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ConformalProjection.java
index 4fd8672176..58582e4e58 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ConformalProjection.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/ConformalProjection.java
@@ -328,4 +328,12 @@ abstract class ConformalProjection extends 
NormalizedProjection {
     final double dy_dφ(final double sinφ, final double cosφ) {
         return (1 / cosφ)  -  eccentricitySquared * cosφ / (1 - 
eccentricitySquared * (sinφ*sinφ));
     }
+
+    final double[] getExpansionFirstTerms() {
+        return new double[]{c2χ, c4χ, c6χ, c8χ};
+    }
+
+    final boolean isUseIterations() {
+        return useIterations;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/EquidistantCylindrical.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/EquidistantCylindrical.java
index ed5269ad00..166af4d24b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/EquidistantCylindrical.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/EquidistantCylindrical.java
@@ -258,4 +258,36 @@ public class EquidistantCylindrical extends 
NormalizedProjection {
         dstPts[dstOff  ] = srcPts[srcOff];
         dstPts[dstOff+1] = y + μ;
     }
+
+    @Override
+    protected String toECMAScript(boolean inverse) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        if (!inverse) {
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tconst φ = src[1];\n" +
+                "\t\tconst sinθ = Math.sin(2*φ);\n" +
+                "\t\tconst cosθ = Math.cos(2*φ);\n" +
+                "\t\tconst y = (((((("+Double.toString(cf7)+"*cosθ + 
"+Double.toString(cf6)+")*cosθ + "+Double.toString(cf5)+")*cosθ + 
"+Double.toString(cf4)+")*cosθ + "+Double.toString(cf3)+")*cosθ + 
"+Double.toString(cf2)+")*cosθ + "+Double.toString(cf1)+")*sinθ + 
"+Double.toString(c0)+"*φ;\n" +
+                "\t\treturn [src[0], y];\n" +
+                "\t}\n"
+                );
+        } else {
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tconst μ = src[1] / "+Double.toString(c0)+";\n" +
+                "\t\tconst sinθ = Math.sin(2*μ);\n" +
+                "\t\tconst cosθ = Math.cos(2*μ);\n" +
+                "\t\tconst y = (((((("+Double.toString(ci7)+"*cosθ + 
"+Double.toString(ci6)+")*cosθ + "+Double.toString(ci5)+")*cosθ + 
"+Double.toString(ci4)+")*cosθ + "+Double.toString(ci3)+")*cosθ + 
"+Double.toString(ci2)+")*cosθ + "+Double.toString(ci1)+")*sinθ;\n" +
+                "\t\treturn [src[0], y + μ];\n" +
+                "\t}\n"
+                );
+        }
+
+        sb.append("}");
+        return sb.toString();
+    }
+
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
index 7428ce5a6a..2e3b05d1b9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
@@ -496,6 +496,91 @@ public class LambertConicConformal extends 
ConformalProjection {
     }
 
 
+    @Override
+    protected String toECMAScript(boolean inverse) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        if (!inverse) {
+            //constants
+            sb.append("\t_eccentricity : 
").append(Double.toString(eccentricity)).append(",\n");
+            sb.append("\t_ANGULAR_TOLERANCE : 
").append(Double.toString(ANGULAR_TOLERANCE)).append(",\n");
+            //utils functions
+            sb.append(
+              "\t_expΨ : function(φ, ℯsinφ) {\n" +
+              "\t\treturn Math.tan(Math.PI/4 + 0.5*φ) * Math.pow((1 - ℯsinφ) / 
(1 + ℯsinφ), 0.5*this._eccentricity);\n" +
+              "\t},\n");
+
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tconst n = " + Double.toString(n) + ";\n" +
+                "\t\tconst θ    = src[0];           // θ = Δλ⋅n\n" +
+                "\t\tconst φ    = src[1];           // Sign may be reversed\n" 
+
+                "\t\tconst absφ = Math.abs(φ);\n" +
+                "\t\tconst sinθ = Math.sin(θ);\n" +
+                "\t\tconst cosθ = Math.cos(θ);\n" +
+                "\t\tlet sinφ = 0.0;\n" +
+                "\t\tlet ρ = 0.0;     // EPSG guide uses \"r\", but we keep 
the symbol from Snyder p. 108 for consistency with PolarStereographic.\n" +
+                "\t\tif (absφ < Math.PI/2) {\n" +
+                "\t\t    sinφ = Math.sin(φ);\n" +
+                "\t\t    ρ = Math.pow(this._expΨ(φ, this._eccentricity*sinφ), 
n);\n" +
+                "\t\t} else if (absφ < Math.PI/2 + this._ANGULAR_TOLERANCE) 
{\n" +
+                "\t\t    sinφ = 1;\n" +
+                "\t\t    ρ = (φ*n >= 0) ? Number.POSITIVE_INFINITY : 0;\n" +
+                "\t\t} else {\n" +
+                "\t\t    ρ = sinφ = Number.NaN;\n" +
+                "\t\t}\n" +
+                "\t\tconst x = ρ * sinθ;\n" +
+                "\t\tconst y = ρ * cosθ;\n" +
+                "\t\treturn [x,y];\n" +
+                "\t}\n");
+        } else {
+
+            //utils functions
+            sb.append(
+              "\t_fastHypot : function(x, y) {\n" +
+              "\t\treturn Math.sqrt(x*x + y*y);\n" +
+              "\t},\n");
+            final double[] exp = getExpansionFirstTerms();
+            sb.append(
+                "\t_φ : function(rexpΨ) {\n" +
+                "\t\tlet φ = (Math.PI/2) - 2*Math.atan(rexpΨ);\n" +
+                "\t\tconst sin_2φ = Math.sin(2*φ);\n" +
+                "\t\tconst cos_2φ = Math.cos(2*φ);\n" +
+                "\t\tφ += sin_2φ * ("+Double.toString(exp[0])+" + cos_2φ * 
("+Double.toString(exp[1])+" + cos_2φ * ("+Double.toString(exp[2])+" + cos_2φ * 
"+Double.toString(exp[3])+")));\n");
+
+            if (!isUseIterations()) {
+                sb.append(
+                    "\t\treturn φ;\n" +
+                    "\t},\n");
+            } else {
+                sb.append(
+                    "\t\tconst hℯ = 0.5 * this._eccentricity;\n" +
+                    "\t\tfor (let it=0; it<this._MAXIMUM_ITERATIONS; it++) 
{\n" +
+                    "\t\t\tconst ℯsinφ = this._eccentricity * Math.sin(φ);\n" +
+                    "\t\t\tconst Δφ = φ - (φ = Math.PI/2 - 2*Math.atan(rexpΨ * 
Math.pow((1 - ℯsinφ)/(1 + ℯsinφ), hℯ)));\n" +
+                    "\t\t\tif (!(Math.abs(Δφ) > this._ITERATION_TOLERANCE)) { 
// Use '!' for accepting NaN.\n" +
+                    "\t\t\t\treturn φ;\n" +
+                    "\t\t\t}\n" +
+                    "\t\t}\n" +
+                    "\t\treturn Number.NaN;\n" +
+                    "\t},\n");
+            }
+
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tconst n = " + Double.toString(n) + ";\n" +
+                "\t\tconst x = src[0];\n" +
+                "\t\tconst y = src[1];\n" +
+                "\t\tconst d0 = Math.atan2(x, y); // Really (x,y), not 
(y,x)\n" +
+                "\t\tconst d1 = -this._φ(Math.pow(this._fastHypot(x, y), 
1.0/n)); // Equivalent to φ(pow(hypot(x,y), -1/n)) but more accurate for 
n>0.\n" +
+                "\t\treturn [d0,d1];\n" +
+                "\t}\n");
+        }
+
+        sb.append("}");
+        return sb.toString();
+    }
     /**
      * Provides the transform equations for the spherical case of the Lambert 
Conformal projection.
      *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LongitudeWraparound.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LongitudeWraparound.java
index a47f4e7790..1d53d76c3b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LongitudeWraparound.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/LongitudeWraparound.java
@@ -34,6 +34,7 @@ import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.privy.DoubleDouble;
 import org.apache.sis.util.privy.Numerics;
 import org.apache.sis.measure.Longitude;
+import org.apache.sis.referencing.ExportableTransform;
 
 
 /**
@@ -68,7 +69,7 @@ import org.apache.sis.measure.Longitude;
  * @see org.apache.sis.referencing.operation.transform.WraparoundTransform
  * @see <a href="https://issues.apache.org/jira/browse/SIS-486";>SIS-486</a>
  */
-final class LongitudeWraparound extends AbstractMathTransform2D implements 
Serializable {
+final class LongitudeWraparound extends AbstractMathTransform2D implements 
Serializable, ExportableTransform {
     /**
      * For cross-version compatibility.
      */
@@ -226,13 +227,18 @@ final class LongitudeWraparound extends 
AbstractMathTransform2D implements Seria
         return inverse;
     }
 
+    @Override
+    public String toECMAScript() throws UnsupportedOperationException {
+        return projection.toECMAScript();
+    }
+
     /**
      * Longitude wraparound applied on reverse projection.
      * This is a copy of {@code NormalizedProjection.Inverse} with longitude 
wraparound added after conversion.
      *
      * @author  Martin Desruisseaux (Geomatys)
      */
-    private static final class Inverse extends AbstractMathTransform2D.Inverse 
implements Serializable {
+    private static final class Inverse extends AbstractMathTransform2D.Inverse 
implements Serializable, ExportableTransform {
         /**
          * For cross-version compatibility.
          */
@@ -312,6 +318,11 @@ final class LongitudeWraparound extends 
AbstractMathTransform2D implements Seria
                 }
             }
         }
+
+        @Override
+        public String toECMAScript() throws UnsupportedOperationException {
+            return forward.projection.toECMAScript(true);
+        }
     }
 
     /*
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
index 29452d770e..f732abb301 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
@@ -482,6 +482,87 @@ subst:  if (variant.spherical || eccentricity == 0) {
         dstPts[dstOff+1] = φ(exp(-y));
     }
 
+    @Override
+    protected String toECMAScript(boolean inverse) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        if (!inverse) {
+            //constants
+            sb.append("\t_ANGULAR_TOLERANCE : 
").append(Double.toString(ANGULAR_TOLERANCE)).append(",\n");
+            sb.append("\t_eccentricity : 
").append(Double.toString(eccentricity)).append(",\n");
+            //utils functions
+            sb.append(
+              "\t_expΨ : function(φ, ℯsinφ) {\n" +
+              "\t\treturn Math.tan(Math.PI/4 + 0.5*φ) * Math.pow((1 - ℯsinφ) / 
(1 + ℯsinφ), 0.5*this._eccentricity);\n" +
+              "\t},\n");
+            sb.append(
+              "\t_copySign : function(x, y) {\n" +
+              "\t\treturn Math.sign(x) === Math.sign(y) ? x : -x;\n" +
+              "\t},\n");
+
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tlet φ = src[1];\n"+
+                "\t\tlet sinφ = Math.sin(φ);\n"+
+                "\t\tlet y = 0.0;\n"+
+                "\t\tlet a = 0.0;\n"+
+                "\t\tif (φ == 0.0) {\n"+
+                "\t\t\ty = φ;\n"+
+                "\t\t} else {\n"+
+                "\t\t\ta = Math.abs(φ);\n"+
+                "\t\t\tif (a < Math.PI / 2.0) {\n"+
+                "\t\t\t\ty = Math.log(this._expΨ(φ, this._eccentricity * 
sinφ));\n"+
+                "\t\t\t} else if (a <= (Math.PI/2 + this._ANGULAR_TOLERANCE)) 
{\n"+
+                "\t\t\t\ty = this._copySign(Number.POSITIVE_INFINITY, φ);\n"+
+                "\t\t\t} else {\n"+
+                "\t\t\t\ty = NaN;\n"+
+                "\t\t\t}\n"+
+                "\t\t}\n"+
+                "\t\treturn [src[0], y];\n" +
+                "\t}\n"
+                );
+
+        } else {
+
+            final double[] exp = getExpansionFirstTerms();
+            sb.append(
+                "\t_φ : function(rexpΨ) {\n" +
+                "\t\tlet φ = (Math.PI/2) - 2*Math.atan(rexpΨ);\n" +
+                "\t\tconst sin_2φ = Math.sin(2*φ);\n" +
+                "\t\tconst cos_2φ = Math.cos(2*φ);\n" +
+                "\t\tφ += sin_2φ * ("+Double.toString(exp[0])+" + cos_2φ * 
("+Double.toString(exp[1])+" + cos_2φ * ("+Double.toString(exp[2])+" + cos_2φ * 
"+Double.toString(exp[3])+")));\n");
+
+            if (!isUseIterations()) {
+                sb.append(
+                    "\t\treturn φ;\n" +
+                    "\t},\n");
+            } else {
+                sb.append(
+                    "\t\tconst hℯ = 0.5 * this._eccentricity;\n" +
+                    "\t\tfor (let it=0; it<this._MAXIMUM_ITERATIONS; it++) 
{\n" +
+                    "\t\t\tconst ℯsinφ = this._eccentricity * Math.sin(φ);\n" +
+                    "\t\t\tconst Δφ = φ - (φ = Math.PI/2 - 2*Math.atan(rexpΨ * 
Math.pow((1 - ℯsinφ)/(1 + ℯsinφ), hℯ)));\n" +
+                    "\t\t\tif (!(Math.abs(Δφ) > this._ITERATION_TOLERANCE)) { 
// Use '!' for accepting NaN.\n" +
+                    "\t\t\t\treturn φ;\n" +
+                    "\t\t\t}\n" +
+                    "\t\t}\n" +
+                    "\t\treturn Number.NaN;\n" +
+                    "\t},\n");
+            }
+
+
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tlet y = src[1];\n"+
+                "\t\treturn [src[0], this._φ(Math.exp(-y))];\n" +
+                "\t}\n"
+                );
+        }
+
+        sb.append("}");
+        return sb.toString();
+    }
 
     /**
      * Provides the transform equations for the spherical case of the Mercator 
projection.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
index 67e21ff34f..25bc92847f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/NormalizedProjection.java
@@ -47,6 +47,7 @@ import 
org.apache.sis.referencing.operation.transform.ContextualParameters;
 import org.apache.sis.referencing.operation.transform.MathTransformProvider;
 import org.apache.sis.referencing.operation.transform.DomainDefinition;
 import org.apache.sis.referencing.operation.provider.MapProjection;
+import org.apache.sis.referencing.ExportableTransform;
 import org.apache.sis.referencing.privy.Formulas;
 import org.apache.sis.system.Modules;
 import org.apache.sis.util.privy.Constants;
@@ -123,7 +124,7 @@ import org.opengis.referencing.ReferenceIdentifier;
  * @see ContextualParameters
  * @see <a href="https://mathworld.wolfram.com/MapProjection.html";>Map 
projections on MathWorld</a>
  */
-public abstract class NormalizedProjection extends AbstractMathTransform2D 
implements Serializable {
+public abstract class NormalizedProjection extends AbstractMathTransform2D 
implements Serializable, ExportableTransform {
     /**
      * For cross-version compatibility.
      */
@@ -771,6 +772,15 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
         return inverse;
     }
 
+    protected String toECMAScript(boolean inverse) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toECMAScript() throws UnsupportedOperationException {
+        return toECMAScript(false);
+    }
+
     /**
      * Reverse of a normalized map projection.
      * Note that a slightly modified copy of this class is in {@code 
LongitudeWraparound.Inverse}.
@@ -778,7 +788,7 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
      *
      * @author  Martin Desruisseaux (Geomatys)
      */
-    private static final class Inverse extends AbstractMathTransform2D.Inverse 
implements Serializable {
+    private static final class Inverse extends AbstractMathTransform2D.Inverse 
implements Serializable, ExportableTransform {
         /**
          * For cross-version compatibility.
          */
@@ -854,6 +864,11 @@ public abstract class NormalizedProjection extends 
AbstractMathTransform2D imple
                 super.tryConcatenate(context);
             }
         }
+
+        @Override
+        public String toECMAScript() throws UnsupportedOperationException {
+            return forward.toECMAScript(true);
+        }
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/PolarStereographic.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/PolarStereographic.java
index 62573b1b42..8fab459424 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/PolarStereographic.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/PolarStereographic.java
@@ -372,6 +372,92 @@ public class PolarStereographic extends 
ConformalProjection {
         dstPts[dstOff+1] = -φ(fastHypot(x, y));
     }
 
+    @Override
+    protected String toECMAScript(boolean inverse) {
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        if (!inverse) {
+            //constants
+            sb.append("\t_eccentricity : 
").append(Double.toString(eccentricity)).append(",\n");
+            //utils functions
+            sb.append(
+              "\t_expΨ : function(φ, ℯsinφ) {\n" +
+              "\t\treturn Math.tan(Math.PI/4 + 0.5*φ) * Math.pow((1 - ℯsinφ) / 
(1 + ℯsinφ), 0.5*this._eccentricity);\n" +
+              "\t},\n");
+        } else {
+            //constants
+            if (isUseIterations()) {
+                sb.append("\t_MAXIMUM_ITERATIONS : 
").append(MAXIMUM_ITERATIONS).append(",\n");
+                sb.append("\t_ITERATION_TOLERANCE : 
").append(Double.toString(ITERATION_TOLERANCE)).append(",\n");
+                sb.append("\t_eccentricity : 
").append(Double.toString(eccentricity)).append(",\n");
+            }
+            //utils functions
+            sb.append(
+              "\t_fastHypot : function(x, y) {\n" +
+              "\t\treturn Math.sqrt(x*x + y*y);\n" +
+              "\t},\n");
+
+
+            final double[] exp = getExpansionFirstTerms();
+            sb.append(
+                "\t_φ : function(rexpΨ) {\n" +
+                "\t\tlet φ = (Math.PI/2) - 2*Math.atan(rexpΨ);\n" +
+                "\t\tconst sin_2φ = Math.sin(2*φ);\n" +
+                "\t\tconst cos_2φ = Math.cos(2*φ);\n" +
+                "\t\tφ += sin_2φ * ("+Double.toString(exp[0])+" + cos_2φ * 
("+Double.toString(exp[1])+" + cos_2φ * ("+Double.toString(exp[2])+" + cos_2φ * 
"+Double.toString(exp[3])+")));\n");
+
+            if (!isUseIterations()) {
+                sb.append(
+                    "\t\treturn φ;\n" +
+                    "\t},\n");
+            } else {
+                sb.append(
+                    "\t\tconst hℯ = 0.5 * this._eccentricity;\n" +
+                    "\t\tfor (let it=0; it<this._MAXIMUM_ITERATIONS; it++) 
{\n" +
+                    "\t\t\tconst ℯsinφ = this._eccentricity * Math.sin(φ);\n" +
+                    "\t\t\tconst Δφ = φ - (φ = Math.PI/2 - 2*Math.atan(rexpΨ * 
Math.pow((1 - ℯsinφ)/(1 + ℯsinφ), hℯ)));\n" +
+                    "\t\t\tif (!(Math.abs(Δφ) > this._ITERATION_TOLERANCE)) { 
// Use '!' for accepting NaN.\n" +
+                    "\t\t\t\treturn φ;\n" +
+                    "\t\t\t}\n" +
+                    "\t\t}\n" +
+                    "\t\treturn Number.NaN;\n" +
+                    "\t},\n");
+            }
+        }
+
+
+        //transform function
+        if (!inverse) {
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tconst θ    = src[0];\n" +
+                "\t\tconst φ    = src[1];\n" +
+                "\t\tconst sinθ = Math.sin(θ);\n" +
+                "\t\tconst cosθ = Math.cos(θ);\n" +
+                "\t\tconst sinφ = Math.sin(φ);\n" +
+                "\t\tconst t = this._expΨ(φ, this._eccentricity*sinφ);\n" +
+                "\t\tconst x = t * sinθ;\n" +
+                "\t\tconst y = t * cosθ;\n" +
+                "\t\treturn [x,y];\n" +
+                "\t}\n"
+                );
+        } else {
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tconst x = src[0];\n" +
+                "\t\tconst y = src[1];\n" +
+                "\t\tconst d0 = Math.atan2(x, y);\n" +
+                "\t\tconst d1 = -this._φ(this._fastHypot(x, y));\n" +
+                "\t\treturn [d0, d1];\n" +
+                "\t}\n"
+                );
+        }
+
+        sb.append("}");
+        return sb.toString();
+    }
 
     /**
      * Provides the transform equations for the spherical case of the polar 
stereographic projection.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
index be6579a88a..b489105465 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/TransverseMercator.java
@@ -733,8 +733,144 @@ public class TransverseMercator extends 
NormalizedProjection {
         throw new 
ProjectionException(Resources.format(Resources.Keys.NoConvergence));
     }
 
+    @Override
+    protected String toECMAScript(boolean inverse) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        if (!inverse) {
+            //constants
+            sb.append("\t_eccentricity : 
").append(Double.toString(eccentricity)).append(",\n");
+            sb.append("\t_cf2 : ").append(Double.toString(cf2)).append(",\n");
+            sb.append("\t_cf4 : ").append(Double.toString(cf4)).append(",\n");
+            sb.append("\t_cf6 : ").append(Double.toString(cf6)).append(",\n");
+            sb.append("\t_cf8 : ").append(Double.toString(cf8)).append(",\n");
+
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tlet dst = new Array("+getTargetDimensions()+");\n" +
+                "\t\tconst λ = src[0];\n" +
+                "\t\tconst φ = src[1];\n" +
+                "\t\tif (Math.abs(λ) > 90 * (Math.PI/180) && 
Math.abs(Math.IEEEremainder(λ, 2*PI)) > 90 * (Math.PI/180)) {\n" +
+                "\t\t\tdst[0] = Number.NaN;\n" +
+                "\t\t\tdst[1] = Number.NaN;\n" +
+                "\t\t} else {\n" +
+                "\t\t\tconst sinλ  = Math.sin(λ);\n" +
+                "\t\t\tconst ℯsinφ = Math.sin(φ) * this._eccentricity;\n" +
+                "\t\t\tconst Q     = Math.asinh(Math.tan(φ)) - 
Math.atanh(ℯsinφ) * this._eccentricity;\n" +
+                "\t\t\tconst coshQ = Math.cosh(Q);\n" +
+                "\t\t\tconst η0    = Math.atanh(sinλ / coshQ);\n" +
+                "\t\t\tconst coshη0 = Math.cosh(η0);\n" +
+                "\t\t\tconst ξ0     = Math.asin(Math.tanh(Q) * coshη0);\n" +
+                "\t\t\tconst sin_2ξ0 = Math.sin(2*ξ0);\n" +
+                "\t\t\tconst cos_2ξ0 = Math.cos(2*ξ0);\n" +
+                "\t\t\tconst sin2 = sin_2ξ0 * sin_2ξ0;\n" +
+                "\t\t\tconst cos2 = cos_2ξ0 * cos_2ξ0;\n" +
+                "\t\t\tconst sin_4ξ0 = sin_2ξ0 * cos_2ξ0;\n" +
+                "\t\t\tconst cos_4ξ0 = (cos2  - sin2)   * 0.5;\n" +
+                "\t\t\tconst sin_6ξ0 = (0.75  - sin2)   * sin_2ξ0;\n" +
+                "\t\t\tconst cos_6ξ0 = (cos2  - 0.75)   * cos_2ξ0;\n" +
+                "\t\t\tconst sin_8ξ0 =          sin_4ξ0 * cos_4ξ0;\n" +
+                "\t\t\tconst cos_8ξ0 =  0.125 - sin_4ξ0 * sin_4ξ0;\n" +
+                "\t\t\tconst sinh_2η0 = Math.sinh(2*η0);\n" +
+                "\t\t\tconst cosh_2η0 = Math.cosh(2*η0);\n" +
+                "\t\t\tconst sinh2 = sinh_2η0 * sinh_2η0;\n" +
+                "\t\t\tconst cosh2 = cosh_2η0 * cosh_2η0;\n" +
+                "\t\t\tconst cosh_4η0 = (cosh2 + sinh2) * 0.5;\n" +
+                "\t\t\tconst sinh_4η0 = cosh_2η0 * sinh_2η0;\n" +
+                "\t\t\tconst cosh_6η0 = cosh_2η0 * (cosh2   - 0.75);\n" +
+                "\t\t\tconst sinh_6η0 = sinh_2η0 * (sinh2   + 0.75);\n" +
+                "\t\t\tconst cosh_8η0 = sinh_4η0 * sinh_4η0 + 0.125;\n" +
+                "\t\t\tconst sinh_8η0 = sinh_4η0 * cosh_4η0;\n" +
+                "\t\t\t// η(λ,φ)\n" +
+                "\t\t\tdst[0] = this._cf8 * cos_8ξ0 * sinh_8η0\n" +
+                "\t\t\t       + this._cf6 * cos_6ξ0 * sinh_6η0\n" +
+                "\t\t\t       + this._cf4 * cos_4ξ0 * sinh_4η0\n" +
+                "\t\t\t       + this._cf2 * cos_2ξ0 * sinh_2η0\n" +
+                "\t\t\t       + η0;\n" +
+                "\t\t\t// ξ(λ,φ)\n" +
+                "\t\t\tdst[1] = this._cf8 * sin_8ξ0 * cosh_8η0\n" +
+                "\t\t\t       + this._cf6 * sin_6ξ0 * cosh_6η0\n" +
+                "\t\t\t       + this._cf4 * sin_4ξ0 * cosh_4η0\n" +
+                "\t\t\t       + this._cf2 * sin_2ξ0 * cosh_2η0\n" +
+                "\t\t\t       + ξ0;\n" +
+                "\t\t}\n" +
+                "\t\treturn dst;\n" +
+                "\t}\n"
+                );
+        } else {
+            //constants
+            sb.append("\t_ITERATION_TOLERANCE : 
").append(Double.toString(ITERATION_TOLERANCE)).append(",\n");
+            sb.append("\t_MAXIMUM_ITERATIONS : 
").append(MAXIMUM_ITERATIONS).append(",\n");
+            sb.append("\t_eccentricity : 
").append(Double.toString(eccentricity)).append(",\n");
+            sb.append("\t_ci2 : ").append(Double.toString(ci2)).append(",\n");
+            sb.append("\t_ci4 : ").append(Double.toString(ci4)).append(",\n");
+            sb.append("\t_ci6 : ").append(Double.toString(ci6)).append(",\n");
+            sb.append("\t_ci8 : ").append(Double.toString(ci8)).append(",\n");
+
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tlet dst = new Array("+getTargetDimensions()+");\n" +
+                "\t\tconst η = src[0];\n" +
+                "\t\tconst ξ = src[1];\n" +
+                "\t\tconst sin_2ξ  = Math.sin(2*ξ);\n" +
+                "\t\tconst cos_2ξ  = Math.cos(2*ξ);\n" +
+                "\t\tconst sinh_2η = Math.sinh(2*η);\n" +
+                "\t\tconst cosh_2η = Math.cosh(2*η);\n" +
+                "\t\tconst sin2 = sin_2ξ * sin_2ξ;\n" +
+                "\t\tconst cos2 = cos_2ξ * cos_2ξ;\n" +
+                "\t\tconst sin_4ξ = sin_2ξ * cos_2ξ;\n" +
+                "\t\tconst cos_4ξ = (cos2  - sin2)   * 0.5;\n" +
+                "\t\tconst sin_6ξ = (0.75  - sin2)   * sin_2ξ;\n" +
+                "\t\tconst cos_6ξ = (cos2  - 0.75)   * cos_2ξ;\n" +
+                "\t\tconst sin_8ξ =          sin_4ξ * cos_4ξ;\n" +
+                "\t\tconst cos_8ξ =  0.125 - sin_4ξ * sin_4ξ;\n" +
+                "\t\t\n" +
+                "\t\tconst sinh2 = sinh_2η * sinh_2η;\n" +
+                "\t\tconst cosh2 = cosh_2η * cosh_2η;\n" +
+                "\t\tconst cosh_4η = (cosh2 + sinh2) * 0.5;\n" +
+                "\t\tconst sinh_4η = cosh_2η * sinh_2η;\n" +
+                "\t\tconst cosh_6η = cosh_2η * (cosh2   - 0.75);\n" +
+                "\t\tconst sinh_6η = sinh_2η * (sinh2   + 0.75);\n" +
+                "\t\tconst cosh_8η = sinh_4η * sinh_4η + 0.125;\n" +
+                "\t\tconst sinh_8η = sinh_4η * cosh_4η;\n" +
+                "\t\tconst ξ0 = ξ - (this._ci8 * sin_8ξ * cosh_8η\n" +
+                "\t\t              + this._ci6 * sin_6ξ * cosh_6η\n" +
+                "\t\t              + this._ci4 * sin_4ξ * cosh_4η\n" +
+                "\t\t              + this._ci2 * sin_2ξ * cosh_2η);\n" +
+                "\t\t\n" +
+                "\t\tconst η0 = η - (this._ci8 * cos_8ξ * sinh_8η\n" +
+                "\t\t              + this._ci6 * cos_6ξ * sinh_6η\n" +
+                "\t\t              + this._ci4 * cos_4ξ * sinh_4η\n" +
+                "\t\t              + this._ci2 * cos_2ξ * sinh_2η);\n" +
+                "\t\t\n" +
+                "\t\tconst β = Math.asin(Math.sin(ξ0) / Math.cosh(η0));\n" +
+                "\t\tconst Q = Math.asinh(Math.tan(β));\n" +
+                "\t\tlet p = this._eccentricity * 
Math.atanh(this._eccentricity * Math.tanh(Q));\n" +
+                "\t\tlet Qp = Q + p;\n" +
+                "\t\tlet found = false;\n" +
+                "\t\tfor (let it=0; !found && it<this._MAXIMUM_ITERATIONS; 
it++) {\n" +
+                "\t\t\tconst c = this._eccentricity * 
Math.atanh(this._eccentricity * Math.tanh(Qp));\n" +
+                "\t\t\tQp = Q + c;\n" +
+                "\t\t\tif (Math.abs(c - p) <= this._ITERATION_TOLERANCE) {\n" +
+                "\t\t\t\tdst[0] = Math.asin(Math.tanh(η0) / Math.cos(β));\n" +
+                "\t\t\t\tdst[1] = Math.atan(Math.sinh(Qp));\n" +
+                "\t\t\t\tfound = true;\n" +
+                "\t\t\t}\n" +
+                "\t\t\tp = c;\n" +
+                "\t\t}\n" +
+                "\t\tif (!found) {\n" +
+                "\t\t\tdst[0] = Number.NaN;\n" +
+                "\t\t\tdst[1] = Number.NaN;\n" +
+                "\t\t}\n" +
+                "\t\treturn dst;\n" +
+                "\t}\n"
+                );
+        }
 
-
+        sb.append("}");
+        return sb.toString();
+    }
 
     /**
      * Provides the transform equations for the spherical case of the 
Transverse Mercator projection.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
index 4619330e0a..3355d04062 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/ConcatenatedTransform.java
@@ -46,6 +46,7 @@ import org.apache.sis.util.privy.Strings;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.io.wkt.FormattableObject;
+import org.apache.sis.referencing.ExportableTransform;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 
@@ -65,7 +66,7 @@ import org.opengis.geometry.MismatchedDimensionException;
  *
  * @see MathTransformFactory#createConcatenatedTransform(MathTransform, 
MathTransform)
  */
-class ConcatenatedTransform extends AbstractMathTransform implements 
Serializable {
+class ConcatenatedTransform extends AbstractMathTransform implements 
Serializable, ExportableTransform {
     /**
      * Serial number for inter-operability with different versions.
      */
@@ -839,6 +840,39 @@ class ConcatenatedTransform extends AbstractMathTransform 
implements Serializabl
         } while (numPts != 0);
     }
 
+    @Override
+    public String toECMAScript() throws UnsupportedOperationException {
+        final int sub1DimSource = transform1.getSourceDimensions();
+        final int sub2DimSource = transform2.getSourceDimensions();
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        if (transform1 instanceof ExportableTransform) {
+            final ExportableTransform exp = (ExportableTransform) transform1;
+            sb.append("\t_sub1 : ").append(exp.toECMAScript().replace("\n", 
"\n\t")).append(",\n");
+        } else {
+            throw new 
UnsupportedOperationException(transform1.getClass().getName() + " is not an 
ExportableTransform.");
+        }
+
+        if (transform2 instanceof ExportableTransform) {
+            final ExportableTransform exp = (ExportableTransform) transform2;
+            sb.append("\t_sub2 : ").append(exp.toECMAScript().replace("\n", 
"\n\t")).append(",\n");
+        } else {
+            throw new 
UnsupportedOperationException(transform2.getClass().getName() + " is not an 
ExportableTransform.");
+        }
+
+        sb.append(
+            "\ttransform : function(src) {\n" +
+            "\t\tsrc = this._sub1.transform(src);\n" +
+            "\t\tsrc = this._sub2.transform(src);\n" +
+            "\t\treturn src;\n" +
+            "\t}\n");
+
+        sb.append("}");
+        return sb.toString();
+    }
+
     /**
      * Gets the derivative of this transform at a point.
      *
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
index 1b72fb547c..d6cb75b204 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
@@ -61,6 +61,7 @@ import static 
org.apache.sis.referencing.operation.provider.MapProjection.SEMI_M
 import static 
org.apache.sis.referencing.operation.provider.MapProjection.SEMI_MINOR;
 import static 
org.apache.sis.referencing.operation.provider.MapProjection.ECCENTRICITY;
 import static 
org.apache.sis.referencing.operation.provider.GeocentricAffineBetweenGeographic.DIMENSION;
+import org.apache.sis.referencing.ExportableTransform;
 
 
 /**
@@ -98,7 +99,7 @@ import static 
org.apache.sis.referencing.operation.provider.GeocentricAffineBetw
  * @version 1.5
  * @since   0.7
  */
-public class EllipsoidToCentricTransform extends AbstractMathTransform 
implements Serializable {
+public class EllipsoidToCentricTransform extends AbstractMathTransform 
implements Serializable, ExportableTransform {
     /**
      * Serial number for inter-operability with different versions.
      */
@@ -769,8 +770,98 @@ next:   while (--numPts >= 0) {
         return false;
     }
 
+    @Override
+    public String toECMAScript() throws UnsupportedOperationException {
+        return toECMAScript(false);
+    }
+
+    private String toECMAScript(boolean inverse) throws 
UnsupportedOperationException {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        if (!inverse) {
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tconst eccentricitySquared = " + 
Double.toString(eccentricitySquared) + ";\n" +
+                "\t\tconst λ     = src[0];// Longitude\n" +
+                "\t\tconst φ     = src[1];// Latitude\n" +
+                "\t\tconst h     = " + (withHeight ? "src[2]" : "0.0") + ";// 
Height above the ellipsoid\n" +
+                "\t\tconst sinφ  = Math.sin(φ);\n" +
+                "\t\tconst ν     = 1.0/Math.sqrt(1 - eccentricitySquared * 
(sinφ*sinφ)); // Prime vertical radius of curvature at latitude φ\n" +
+                "\t\tconst rcosφ = (ν + h) * Math.cos(φ);\n" +
+                "\t\tconst d0 = rcosφ * Math.cos(λ); // X: Toward prime 
meridian\n" +
+                "\t\tconst d1 = rcosφ * Math.sin(λ); // Y: Toward 90° east\n" +
+                "\t\tconst d2 = (ν * (1 - eccentricitySquared) + h) * sinφ; // 
Z: Toward north pole\n" +
+                "\t\treturn [d0,d1,d2];\n" +
+                "\t}\n"
+                );
+        } else {
+            //constants
+            sb.append("\t_ANGULAR_TOLERANCE : 
").append(Double.toString(Formulas.ANGULAR_TOLERANCE)).append(",\n");
+            sb.append("\t_MAXIMUM_ITERATIONS : 
").append(Formulas.MAXIMUM_ITERATIONS).append(",\n");
+            sb.append("\t_eccentricitySquared : 
").append(Double.toString(eccentricitySquared)).append(",\n");
+            sb.append("\t_axisRatio : 
").append(Double.toString(axisRatio)).append(",\n");
+            //utils functions
+            sb.append(
+              "\t_copySign : function(x, y) {\n" +
+              "\t\treturn Math.sign(x) === Math.sign(y) ? x : -x;\n" +
+              "\t},\n");
 
+            sb.append(
+                "\ttransform : function(src){\n" +
+                "\t\tlet dst = new Array("+getTargetDimensions()+");\n" +
+                "\t\tconst X = src[0];\n" +
+                "\t\tconst Y = src[1];\n" +
+                "\t\tconst Z = src[2];\n" +
+                "\t\tconst p = Math.hypot(X, Y);\n" +
+                "\t\tconst tanq  = Z / (p*this._axisRatio);\n" +
+                "\t\tconst cos2q = 1.0/(1.0 + tanq*tanq);\n" +
+                "\t\tconst sin2q = 1.0 - cos2q;\n" +
+                "\t\tlet φ = Math.atan((Z + 
this._copySign(this._eccentricitySquared * sin2q*Math.sqrt(sin2q), tanq) / 
this._axisRatio) /\n" +
+                "                (p -          this._eccentricitySquared * 
cos2q*Math.sqrt(cos2q)));\n" +
+                "\t\t\n");
+            if (!useIterations) {
+                sb.append(
+                    "\t\tdst[0] = Math.atan2(Y, X);\n" +
+                    "\t\tdst[1] = φ;\n");
+                if (withHeight) {
+                    sb.append(
+                        "\t\tconst sinφ = Math.sin(φ);\n" +
+                        "\t\tconst ν = 1.0/Math.sqrt(1 - 
this._eccentricitySquared * (sinφ*sinφ));\n" +
+                        "\t\tdst[2] = p/Math.cos(φ) - ν;\n");
+                }
+            } else {
+                sb.append(
+                    "\t\tlet found = false;\n" +
+                    "\t\tfor (let it = this._MAXIMUM_ITERATIONS; !found && it 
>= 0; it--) {\n" +
+                    "\t\t\tconst sinφ = Math.sin(φ);\n" +
+                    "\t\t\tconst ν = 1/sqrt(1 - this._eccentricitySquared * 
(sinφ*sinφ));\n" +
+                    "\t\t\tconst Δφ = φ - (φ = atan((Z + 
this._eccentricitySquared * ν * sinφ) / p));\n" +
+                    "\t\t\tif (!(abs(Δφ) >= this._ANGULAR_TOLERANCE * 
(Math.PI/180) * 0.25)) { // Use ! for accepting NaN.\n" +
+                    "\t\t\t\tdst[0] = Math.atan2(Y, X);\n" +
+                    "\t\t\t\tdst[1] = φ;\n");
+                if (withHeight) {
+                    sb.append(
+                        "\t\t\t\tdst[2] = p/Math.cos(φ) - ν;\n");
+                }
+                sb.append(
+                    "\t\t\t\tfound = true;\n" +
+                    "\t\t\t}\n" +
+                    "\t\t}\n" +
+                    "\t\tif (!found) {\n" +
+                    "\t\t\tdst[0] = Number.NaN;\n" +
+                    "\t\t\tdst[1] = Number.NaN;\n" +
+                    "\t\t}\n"
+                    );
+            }
+            sb.append(
+                "\t\treturn dst;\n" +
+                "\t}\n");
+        }
 
+        sb.append("}");
+        return sb.toString();
+    }
 
     /**
      * Converts Cartesian coordinates (<var>X</var>,<var>Y</var>,<var>Z</var>)
@@ -778,7 +869,7 @@ next:   while (--numPts >= 0) {
      *
      * @author  Martin Desruisseaux (IRD, Geomatys)
      */
-    private static final class Inverse extends AbstractMathTransform.Inverse 
implements Serializable {
+    private static final class Inverse extends AbstractMathTransform.Inverse 
implements Serializable, ExportableTransform {
         /**
          * Serial number for inter-operability with different versions.
          */
@@ -961,6 +1052,12 @@ next:   while (--numPts >= 0) {
             }
             return index;
         }
+
+
+        @Override
+        public String toECMAScript() throws UnsupportedOperationException {
+            return forward.toECMAScript(true);
+        }
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LinearTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LinearTransform.java
index 52fb32f769..693b7f00fb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LinearTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/LinearTransform.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.referencing.operation.transform;
 
+import org.apache.sis.referencing.ExportableTransform;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
@@ -71,7 +72,7 @@ import 
org.opengis.referencing.operation.NoninvertibleTransformException;
  *
  * @since 0.4
  */
-public interface LinearTransform extends MathTransform {
+public interface LinearTransform extends MathTransform, ExportableTransform{
     /**
      * Returns {@code true} if this transform is affine.
      * An affine transform preserves parallelism and has the same
@@ -134,4 +135,54 @@ public interface LinearTransform extends MathTransform {
      */
     @Override
     LinearTransform inverse() throws NoninvertibleTransformException;
+
+    @Override
+    public default String toECMAScript() throws UnsupportedOperationException {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        sb.append("\ttransform : function(src) {\n");
+        sb.append("\t\tlet dst = new 
Array(").append(getTargetDimensions()).append(");\n");
+
+
+        final Matrix matrix = getMatrix();
+        sb.append("\t\t/*\n\t\t").append(matrix.toString().replaceAll("\n", 
"\n\t\t")).append("*/\n");
+
+        final int sourceDimensions = getSourceDimensions();
+        final int numCol = matrix.getNumCol();
+        final int targetDimensions = getTargetDimensions();
+
+        for (int k = 0; k < targetDimensions; k++) {
+            sb.append("\t\tdst[").append(k).append("] = ");
+            boolean first = true;
+            for (int c = 0; c < numCol; c++) {
+                final double element = matrix.getElement(k, c);
+                if (element != 0.0) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        sb.append(" + ");
+                    }
+                    if (c >= sourceDimensions) {
+                        //implicite  * 1
+                        sb.append(element);
+                    } else {
+                        sb.append(element).append(" * 
src[").append(c).append("]");
+                    }
+                }
+            }
+            if (first) {
+                //all matrix row values are 0.0
+                sb.append("0.0");
+            }
+
+            sb.append(";\n");
+        }
+        sb.append("\t\treturn dst;\n\t}\n");
+
+        sb.append("}");
+        return sb.toString();
+    }
+
+
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
index 04746075a6..56ce03283f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/PassThroughTransform.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.util.BitSet;
 import java.io.Serializable;
 import java.lang.reflect.Array;
+import java.util.List;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.operation.Matrix;
@@ -38,6 +39,7 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.privy.Numerics;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.io.wkt.Formatter;
+import org.apache.sis.referencing.ExportableTransform;
 import org.apache.sis.util.resources.Errors;
 
 // Specific to the main branch:
@@ -75,7 +77,7 @@ import org.opengis.geometry.MismatchedDimensionException;
  *
  * @since 0.5
  */
-public class PassThroughTransform extends AbstractMathTransform implements 
Serializable {
+public class PassThroughTransform extends AbstractMathTransform implements 
Serializable, ExportableTransform {
     /**
      * Serial number for inter-operability with different versions.
      */
@@ -676,6 +678,47 @@ public class PassThroughTransform extends 
AbstractMathTransform implements Seria
         return inverse;
     }
 
+    @Override
+    public String toECMAScript() throws UnsupportedOperationException {
+
+        final int subDimSource = subTransform.getSourceDimensions();
+        final int subDimTarget = subTransform.getTargetDimensions();
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{\n");
+
+        final List<MathTransform> steps = 
MathTransforms.getSteps(subTransform);
+
+        final StringBuilder trsSb = new StringBuilder();
+        trsSb.append(
+            "\ttransform : function(src) {\n" +
+            "\t\tlet subsrc = [...src.slice(" + firstAffectedCoordinate +"," + 
(firstAffectedCoordinate+subDimSource) +")];\n");
+
+        for (int i = 0, n = steps.size(); i < n; i++) {
+            final MathTransform step = steps.get(i);
+
+            final String stepObj;
+            if (step instanceof ExportableTransform) {
+                final ExportableTransform exp = (ExportableTransform) step;
+                stepObj = exp.toECMAScript();
+            } else {
+                throw new 
UnsupportedOperationException(step.getClass().getName() + " is not an 
ExportableTransform.");
+            }
+            sb.append("\t_step").append(i).append(" : 
").append(stepObj.replaceAll("\n", "\n\t")).append(",\n");
+            trsSb.append("\t\tsubsrc = 
this._step").append(i).append(".transform(subsrc);\n");
+        }
+
+        trsSb.append(
+            "\t\tconst before = src.slice(0," +firstAffectedCoordinate+");\n"+
+            "\t\tconst after = src.slice(-"+numTrailingCoordinates+");\n"+
+            "\t\treturn before.concat(subsrc).concat(after);\n" +
+            "\t}\n");
+
+        sb.append(trsSb.toString());
+        sb.append("}");
+        return sb.toString();
+    }
+
     /**
      * If the given matrix to be concatenated to this transform, can be 
concatenated to the
      * sub-transform instead, returns the matrix to be concatenated to the 
sub-transform.

Reply via email to