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

jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 250d13c566 [CALCITE-4294] Use JTS and proj4j rather than ESRI as the 
underlying library for geospatial (ST_) functions
250d13c566 is described below

commit 250d13c566913ba541ddd7ac4c4443a11a1a976b
Author: Bertil Chapuis <[email protected]>
AuthorDate: Sun Aug 21 16:08:30 2022 +0200

    [CALCITE-4294] Use JTS and proj4j rather than ESRI as the underlying 
library for geospatial (ST_) functions
    
    Prefix classes with SpatialType (ST) instead of Geo.
    
    Adapt tests to JTS outputs. We now  round the floating point
    values in geometries, because floating point values have
    slightly different least-significant digits when run under
    Java 8 and Java 11.
    
    Add method TestUtil.roundGeom(String, int), to allow the
    aforementioned rounding (Julian Hyde).
    
    Unofficially add function ST_AsEWKT for use in tests; it
    will be officially added (with tests and documentation)
    in [CALCITE-5262].
    
    Close apache/calcite#2879
---
 bom/build.gradle.kts                               |   4 +-
 core/build.gradle.kts                              |   4 +-
 .../adapter/enumerable/RexToLixTranslator.java     |   8 +-
 .../apache/calcite/jdbc/JavaTypeFactoryImpl.java   |   4 +-
 .../org/apache/calcite/rel/rules/SpatialRules.java |  37 +-
 .../java/org/apache/calcite/rex/RexBuilder.java    |   4 +-
 .../java/org/apache/calcite/rex/RexLiteral.java    |   8 +-
 .../org/apache/calcite/runtime/GeoFunctions.java   | 595 ---------------------
 .../org/apache/calcite/runtime/Geometries.java     | 293 ----------
 .../calcite/runtime/ProjectionTransformer.java     | 166 ++++++
 .../calcite/runtime/SpatialTypeFunctions.java      | 553 +++++++++++++++++++
 .../apache/calcite/runtime/SpatialTypeUtils.java   | 225 ++++++++
 ...Functions.java => SqlSpatialTypeFunctions.java} |  29 +-
 .../sql/type/JavaToSqlTypeConversionRules.java     |   4 +-
 .../apache/calcite/sql/util/SqlOperatorTables.java |   8 +-
 .../org/apache/calcite/util/BuiltInMethod.java     |   4 +-
 .../calcite/adapter/enumerable/EnumUtilsTest.java  |   6 +-
 .../calcite/runtime/SpatialTypeUtilsTest.java      |  48 ++
 .../java/org/apache/calcite/test/JdbcTest.java     |   8 +-
 .../org/apache/calcite/test/RelOptRulesTest.java   |   2 +-
 .../org/apache/calcite/test/RelOptRulesTest.xml    |  10 +-
 core/src/test/resources/sql/spatial.iq             | 205 ++++---
 .../adapter/geode/rel/GeodeSchemaFactory.java      |   4 +-
 gradle.properties                                  |   4 +-
 src/main/config/checkstyle/suppressions.xml        |   2 +-
 .../org/apache/calcite/test/CalciteAssert.java     |  10 +-
 .../java/org/apache/calcite/util/TestUtil.java     |  63 +++
 .../java/org/apache/calcite/util/TestUtilTest.java |  20 +
 28 files changed, 1256 insertions(+), 1072 deletions(-)

diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index b35d4803d9..019994894d 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -55,7 +55,9 @@ dependencies {
         apiv("com.beust:jcommander")
         apiv("org.checkerframework:checker-qual", "checkerframework")
         apiv("com.datastax.oss:java-driver-core", "cassandra-java-driver-core")
-        apiv("com.esri.geometry:esri-geometry-api")
+        apiv("org.locationtech.jts:jts-core")
+        apiv("org.locationtech.jts.io:jts-io-common")
+        apiv("org.locationtech.proj4j:proj4j")
         apiv("com.fasterxml.jackson.core:jackson-databind")
         apiv("com.github.kstyrc:embedded-redis")
         apiv("com.github.stephenc.jcip:jcip-annotations")
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 6c31bfe20f..dc671be395 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -46,7 +46,9 @@ val testMysql by 
configurations.creating(integrationTestConfig)
 dependencies {
     api(project(":linq4j"))
 
-    api("com.esri.geometry:esri-geometry-api")
+    api("org.locationtech.jts:jts-core")
+    api("org.locationtech.jts.io:jts-io-common")
+    api("org.locationtech.proj4j:proj4j")
     api("com.fasterxml.jackson.core:jackson-annotations")
     api("com.google.errorprone:error_prone_annotations")
     api("com.google.guava:guava")
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index 0e3b4675e0..5f2593011f 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
@@ -50,8 +50,7 @@ import org.apache.calcite.rex.RexSubQuery;
 import org.apache.calcite.rex.RexTableInputRef;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexVisitor;
-import org.apache.calcite.runtime.GeoFunctions;
-import org.apache.calcite.runtime.Geometries;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.schema.FunctionContext;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlOperator;
@@ -69,6 +68,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
+import org.locationtech.jts.geom.Geometry;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
@@ -802,9 +802,9 @@ public class RexToLixTranslator implements 
RexVisitor<RexToLixTranslator.Result>
               literal.getValueAs(byte[].class),
               byte[].class));
     case GEOMETRY:
-      final Geometries.Geom geom = 
requireNonNull(literal.getValueAs(Geometries.Geom.class),
+      final Geometry geom = requireNonNull(literal.getValueAs(Geometry.class),
           () -> "getValueAs(Geometries.Geom) for " + literal);
-      final String wkt = GeoFunctions.ST_AsWKT(geom);
+      final String wkt = SpatialTypeFunctions.ST_AsWKT(geom);
       return Expressions.call(null, BuiltInMethod.ST_GEOM_FROM_TEXT.method,
           Expressions.constant(wkt));
     case SYMBOL:
diff --git 
a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java 
b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java
index ae9acb8693..6a12cc2378 100644
--- a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java
@@ -27,7 +27,6 @@ import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelDataTypeFieldImpl;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.rel.type.RelRecordType;
-import org.apache.calcite.runtime.Geometries;
 import org.apache.calcite.runtime.Unit;
 import org.apache.calcite.sql.type.BasicSqlType;
 import org.apache.calcite.sql.type.IntervalSqlType;
@@ -39,6 +38,7 @@ import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
+import org.locationtech.jts.geom.Geometry;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
@@ -216,7 +216,7 @@ public class JavaTypeFactoryImpl
       case VARBINARY:
         return ByteString.class;
       case GEOMETRY:
-        return Geometries.Geom.class;
+        return Geometry.class;
       case SYMBOL:
         return Enum.class;
       case ANY:
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SpatialRules.java 
b/core/src/main/java/org/apache/calcite/rel/rules/SpatialRules.java
index 41fa39a7ee..0927c2c642 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/SpatialRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/SpatialRules.java
@@ -28,20 +28,20 @@ import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
-import org.apache.calcite.runtime.GeoFunctions;
-import org.apache.calcite.runtime.Geometries;
 import org.apache.calcite.runtime.HilbertCurve2D;
 import org.apache.calcite.runtime.SpaceFillingCurve2D;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.tools.RelBuilder;
 
-import com.esri.core.geometry.Envelope;
-import com.esri.core.geometry.Point;
 import com.google.common.collect.ImmutableList;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.immutables.value.Value;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.Point;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
@@ -101,12 +101,12 @@ public abstract class SpatialRules {
       FilterHilbertRule.Config.DEFAULT.toRule();
 
   /** Returns a geometry if an expression is constant, null otherwise. */
-  private static Geometries.@Nullable Geom constantGeom(RexNode e) {
+  private static @Nullable Geometry constantGeom(RexNode e) {
     switch (e.getKind()) {
     case CAST:
       return constantGeom(((RexCall) e).getOperands().get(0));
     case LITERAL:
-      return (Geometries.Geom) ((RexLiteral) e).getValue();
+      return (Geometry) ((RexLiteral) e).getValue();
     default:
       return null;
     }
@@ -198,14 +198,14 @@ public abstract class SpatialRules {
         RexInputRef ref, RexCall hilbert) {
       final RexNode op0;
       final RexNode op1;
-      final Geometries.Geom g0;
+      final Geometry g0;
       switch (conjunction.getKind()) {
       case ST_DWITHIN:
         final RexCall within = (RexCall) conjunction;
         op0 = within.operands.get(0);
         g0 = constantGeom(op0);
         op1 = within.operands.get(1);
-        final Geometries.Geom g1 = constantGeom(op1);
+        final Geometry g1 = constantGeom(op1);
         if (RexUtil.isLiteral(within.operands.get(2), true)) {
           final Number distance = requireNonNull(
               (Number) value(within.operands.get(2)),
@@ -277,28 +277,27 @@ public abstract class SpatialRules {
      * is 0. But usually returns a list of ranges,
      * {@code ref BETWEEN c1 AND c2 OR ref BETWEEN c3 AND c4}. */
     private static RexNode hilbertPredicate(RexBuilder rexBuilder,
-        RexInputRef ref, Geometries.Geom g, Number distance) {
-      if (distance.doubleValue() == 0D
-          && Geometries.type(g.g()) == Geometries.Type.POINT) {
-        final Point p = (Point) g.g();
+        RexInputRef ref, Geometry g, Number distance) {
+      if (distance.doubleValue() == 0D && g instanceof Point) {
+        final Point p = (Point) g;
         final HilbertCurve2D hilbert = new HilbertCurve2D(8);
         final long index = hilbert.toIndex(p.getX(), p.getY());
         return rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, ref,
             rexBuilder.makeExactLiteral(BigDecimal.valueOf(index)));
       }
-      final Geometries.Geom g2 =
-          GeoFunctions.ST_Buffer(g, distance.doubleValue());
+      final Geometry g2 =
+          SpatialTypeFunctions.ST_Buffer(g, distance.doubleValue());
       return hilbertPredicate(rexBuilder, ref, g2);
     }
 
     private static RexNode hilbertPredicate(RexBuilder rexBuilder,
-        RexInputRef ref, Geometries.Geom g2) {
-      final Geometries.Geom g3 = GeoFunctions.ST_Envelope(g2);
-      final Envelope env = (Envelope) g3.g();
+        RexInputRef ref, Geometry g2) {
+      final Geometry g3 = SpatialTypeFunctions.ST_Envelope(g2);
+      final Envelope env =  g3.getEnvelopeInternal();
       final HilbertCurve2D hilbert = new HilbertCurve2D(8);
       final List<SpaceFillingCurve2D.IndexRange> ranges =
-          hilbert.toRanges(env.getXMin(), env.getYMin(), env.getXMax(),
-              env.getYMax(), new SpaceFillingCurve2D.RangeComputeHints());
+          hilbert.toRanges(env.getMinX(), env.getMinY(), env.getMaxX(),
+              env.getMaxY(), new SpaceFillingCurve2D.RangeComputeHints());
       final List<RexNode> nodes = new ArrayList<>();
       for (SpaceFillingCurve2D.IndexRange range : ranges) {
         final BigDecimal lowerBd = BigDecimal.valueOf(range.lower());
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java 
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index b4d8e66f1d..f41f03a19e 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -27,7 +27,6 @@ import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.runtime.FlatLists;
-import org.apache.calcite.runtime.Geometries;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlIntervalQualifier;
@@ -60,6 +59,7 @@ import com.google.common.collect.TreeRangeSet;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.checkerframework.checker.nullness.qual.PolyNull;
+import org.locationtech.jts.geom.Geometry;
 
 import java.math.BigDecimal;
 import java.math.MathContext;
@@ -1834,7 +1834,7 @@ public class RexBuilder {
       return typeFactory.createSqlType(SqlTypeName.BINARY,
           ((ByteString) value).length());
     }
-    if (value instanceof Geometries.Geom) {
+    if (value instanceof Geometry) {
       return typeFactory.createSqlType(SqlTypeName.GEOMETRY);
     }
     throw new AssertionError("unknown type " + value.getClass());
diff --git a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java 
b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
index 1d22522d19..783f7558dc 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
@@ -25,8 +25,7 @@ import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.runtime.FlatLists;
-import org.apache.calcite.runtime.GeoFunctions;
-import org.apache.calcite.runtime.Geometries;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperator;
@@ -51,6 +50,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
 import org.checkerframework.checker.nullness.qual.PolyNull;
 import org.checkerframework.checker.nullness.qual.RequiresNonNull;
 import org.checkerframework.dataflow.qual.Pure;
+import org.locationtech.jts.geom.Geometry;
 
 import java.io.PrintWriter;
 import java.math.BigDecimal;
@@ -384,7 +384,7 @@ public class RexLiteral extends RexNode {
     case MULTISET:
       return value instanceof List;
     case GEOMETRY:
-      return value instanceof Geometries.Geom;
+      return value instanceof Geometry;
     case ANY:
       // Literal of type ANY is not legal. "CAST(2 AS ANY)" remains
       // an integer literal surrounded by a cast function.
@@ -724,7 +724,7 @@ public class RexLiteral extends RexNode {
               sb3.append(list.get(i).computeDigest(includeType))));
       break;
     case GEOMETRY:
-      final String wkt = GeoFunctions.ST_AsWKT((Geometries.Geom) 
castNonNull(value));
+      final String wkt = SpatialTypeFunctions.ST_AsWKT((Geometry) 
castNonNull(value));
       sb.append(wkt);
       break;
     default:
diff --git a/core/src/main/java/org/apache/calcite/runtime/GeoFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/GeoFunctions.java
deleted file mode 100644
index 87291b5746..0000000000
--- a/core/src/main/java/org/apache/calcite/runtime/GeoFunctions.java
+++ /dev/null
@@ -1,595 +0,0 @@
-/*
- * 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.calcite.runtime;
-
-import org.apache.calcite.linq4j.AbstractEnumerable;
-import org.apache.calcite.linq4j.Enumerator;
-import org.apache.calcite.linq4j.function.Deterministic;
-import org.apache.calcite.linq4j.function.Experimental;
-import org.apache.calcite.linq4j.function.Hints;
-import org.apache.calcite.linq4j.function.SemiStrict;
-import org.apache.calcite.linq4j.function.Strict;
-import org.apache.calcite.runtime.Geometries.CapStyle;
-import org.apache.calcite.runtime.Geometries.Geom;
-import org.apache.calcite.runtime.Geometries.JoinStyle;
-
-import com.esri.core.geometry.Envelope;
-import com.esri.core.geometry.Geometry;
-import com.esri.core.geometry.GeometryEngine;
-import com.esri.core.geometry.Line;
-import com.esri.core.geometry.OperatorBoundary;
-import com.esri.core.geometry.Point;
-import com.esri.core.geometry.Polygon;
-import com.esri.core.geometry.Polyline;
-import com.esri.core.geometry.SpatialReference;
-import com.esri.core.geometry.WktExportFlags;
-import com.esri.core.geometry.WktImportFlags;
-
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-import java.math.BigDecimal;
-import java.util.Objects;
-
-import static org.apache.calcite.runtime.Geometries.NO_SRID;
-import static org.apache.calcite.runtime.Geometries.bind;
-import static org.apache.calcite.runtime.Geometries.buffer;
-import static org.apache.calcite.runtime.Geometries.envelope;
-import static org.apache.calcite.runtime.Geometries.intersects;
-import static org.apache.calcite.runtime.Geometries.makeLine;
-import static org.apache.calcite.runtime.Geometries.point;
-import static org.apache.calcite.runtime.Geometries.todo;
-
-/**
- * Helper methods to implement Geo-spatial functions in generated code.
- *
- * <p>Remaining tasks:
- *
- * <ul>
- *   <li>Determine type code for
- *   {@link org.apache.calcite.sql.type.ExtraSqlTypes#GEOMETRY}
- *   <li>Should we create aliases for functions in upper-case?
- *   Without ST_ prefix?
- *   <li>Consider adding spatial literals, e.g. `GEOMETRY 'POINT (30 10)'`
- *   <li>Integer arguments, e.g. SELECT ST_MakePoint(1, 2, 1.5),
- *     ST_MakePoint(1, 2)
- *   <li>Are GEOMETRY values comparable? If so add ORDER BY test
- *   <li>We have to add 'Z' to create 3D objects. This is inconsistent with
- *   PostGIS. Who is right? At least document the difference.
- *   <li>Should add GeometryEngine.intersects; similar to disjoint etc.
- *   <li>Make {@link #ST_MakeLine(Geom, Geom)} varargs</li>
- * </ul>
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-@Deterministic
-@Strict
-@Experimental
-public class GeoFunctions {
-
-  private GeoFunctions() {}
-
-  // Geometry conversion functions (2D and 3D) ================================
-
-  public static String ST_AsText(Geom g) {
-    return ST_AsWKT(g);
-  }
-
-  public static String ST_AsWKT(Geom g) {
-    return GeometryEngine.geometryToWkt(g.g(),
-        WktExportFlags.wktExportDefaults);
-  }
-
-  public static @Nullable Geom ST_GeomFromText(String s) {
-    return ST_GeomFromText(s, NO_SRID);
-  }
-
-  public static @Nullable Geom ST_GeomFromText(String s, int srid) {
-    final Geometry g = fromWkt(s,
-        WktImportFlags.wktImportDefaults, Geometry.Type.Unknown);
-    return g == null ? null : bind(g, srid);
-  }
-
-  public static @Nullable Geom ST_LineFromText(String s) {
-    return ST_GeomFromText(s, NO_SRID);
-  }
-
-  public static @Nullable Geom ST_LineFromText(String wkt, int srid) {
-    final Geometry g = fromWkt(wkt,
-        WktImportFlags.wktImportDefaults,
-        Geometry.Type.Line);
-    return g == null ? null : bind(g, srid);
-  }
-
-  public static @Nullable Geom ST_MPointFromText(String s) {
-    return ST_GeomFromText(s, NO_SRID);
-  }
-
-  public static @Nullable Geom ST_MPointFromText(String wkt, int srid) {
-    final Geometry g = fromWkt(wkt,
-        WktImportFlags.wktImportDefaults,
-        Geometry.Type.MultiPoint);
-    return g == null ? null : bind(g, srid);
-  }
-
-  public static @Nullable Geom ST_PointFromText(String s) {
-    return ST_GeomFromText(s, NO_SRID);
-  }
-
-  public static @Nullable Geom ST_PointFromText(String wkt, int srid) {
-    final Geometry g =
-        fromWkt(wkt, WktImportFlags.wktImportDefaults, Geometry.Type.Point);
-    return g == null ? null : bind(g, srid);
-  }
-
-  public static @Nullable Geom ST_PolyFromText(String s) {
-    return ST_GeomFromText(s, NO_SRID);
-  }
-
-  public static @Nullable Geom ST_PolyFromText(String wkt, int srid) {
-    final Geometry g = fromWkt(wkt,
-        WktImportFlags.wktImportDefaults,
-        Geometry.Type.Polygon);
-    return g == null ? null : bind(g, srid);
-  }
-
-  public static @Nullable Geom ST_MLineFromText(String s) {
-    return ST_GeomFromText(s, NO_SRID);
-  }
-
-  public static @Nullable Geom ST_MLineFromText(String wkt, int srid) {
-    final Geometry g = fromWkt(wkt,
-        WktImportFlags.wktImportDefaults,
-        Geometry.Type.Unknown); // NOTE: there is no Geometry.Type.MultiLine
-    return g == null ? null : bind(g, srid);
-  }
-
-  public static @Nullable Geom ST_MPolyFromText(String s) {
-    return ST_GeomFromText(s, NO_SRID);
-  }
-
-  public static @Nullable Geom ST_MPolyFromText(String wkt, int srid) {
-    final Geometry g = fromWkt(wkt,
-        WktImportFlags.wktImportDefaults,
-        Geometry.Type.Unknown); // NOTE: there is no Geometry.Type.MultiPolygon
-    return g == null ? null : bind(g, srid);
-  }
-
-  // Geometry creation functions ==============================================
-
-  /** Calculates a regular grid of polygons based on {@code geom}. */
-  private static void ST_MakeGrid(final Geom geom,
-      final BigDecimal deltaX, final BigDecimal deltaY) {
-    // This is a dummy function. We cannot include table functions in this
-    // package, because they have too many dependencies. See the real 
definition
-    // in SqlGeoFunctions.
-  }
-
-  /** Calculates a regular grid of points based on {@code geom}. */
-  private static void ST_MakeGridPoints(final Geom geom,
-      final BigDecimal deltaX, final BigDecimal deltaY) {
-    // This is a dummy function. We cannot include table functions in this
-    // package, because they have too many dependencies. See the real 
definition
-    // in SqlGeoFunctions.
-  }
-
-  /** Creates a rectangular Polygon. */
-  public static Geom ST_MakeEnvelope(BigDecimal xMin, BigDecimal yMin,
-      BigDecimal xMax, BigDecimal yMax, int srid) {
-    Geom geom = ST_GeomFromText("POLYGON(("
-        + xMin + " " + yMin + ", "
-        + xMin + " " + yMax + ", "
-        + xMax + " " + yMax + ", "
-        + xMax + " " + yMin + ", "
-        + xMin + " " + yMin + "))", srid);
-    return Objects.requireNonNull(geom, "geom");
-  }
-
-  /** Creates a rectangular Polygon. */
-  public static Geom ST_MakeEnvelope(BigDecimal xMin, BigDecimal yMin,
-      BigDecimal xMax, BigDecimal yMax) {
-    return ST_MakeEnvelope(xMin, yMin, xMax, yMax, NO_SRID);
-  }
-
-  /** Creates a line-string from the given POINTs (or MULTIPOINTs). */
-  @Hints({"SqlKind:ST_MAKE_LINE"})
-  public static Geom ST_MakeLine(Geom geom1, Geom geom2) {
-    return makeLine(geom1, geom2);
-  }
-
-  @Hints({"SqlKind:ST_MAKE_LINE"})
-  public static Geom ST_MakeLine(Geom geom1, Geom geom2, Geom geom3) {
-    return makeLine(geom1, geom2, geom3);
-  }
-
-  @Hints({"SqlKind:ST_MAKE_LINE"})
-  public static Geom ST_MakeLine(Geom geom1, Geom geom2, Geom geom3,
-      Geom geom4) {
-    return makeLine(geom1, geom2, geom3, geom4);
-  }
-
-  @Hints({"SqlKind:ST_MAKE_LINE"})
-  public static Geom ST_MakeLine(Geom geom1, Geom geom2, Geom geom3,
-      Geom geom4, Geom geom5) {
-    return makeLine(geom1, geom2, geom3, geom4, geom5);
-  }
-
-  @Hints({"SqlKind:ST_MAKE_LINE"})
-  public static Geom ST_MakeLine(Geom geom1, Geom geom2, Geom geom3,
-      Geom geom4, Geom geom5, Geom geom6) {
-    return makeLine(geom1, geom2, geom3, geom4, geom5, geom6);
-  }
-
-  /** Alias for {@link #ST_Point(BigDecimal, BigDecimal)}. */
-  @Hints({"SqlKind:ST_POINT"})
-  public static Geom ST_MakePoint(BigDecimal x, BigDecimal y) {
-    return ST_Point(x, y);
-  }
-
-  /** Alias for {@link #ST_Point(BigDecimal, BigDecimal, BigDecimal)}. */
-  @Hints({"SqlKind:ST_POINT3"})
-  public static Geom ST_MakePoint(BigDecimal x, BigDecimal y, BigDecimal z) {
-    return ST_Point(x, y, z);
-  }
-
-  /** Constructs a 2D point from coordinates. */
-  @Hints({"SqlKind:ST_POINT"})
-  public static Geom ST_Point(BigDecimal x, BigDecimal y) {
-    // NOTE: Combine the double and BigDecimal variants of this function
-    return point(x.doubleValue(), y.doubleValue());
-  }
-
-  /** Constructs a 3D point from coordinates. */
-  @Hints({"SqlKind:ST_POINT3"})
-  public static Geom ST_Point(BigDecimal x, BigDecimal y, BigDecimal z) {
-    final Geometry g = new Point(x.doubleValue(), y.doubleValue(),
-        z.doubleValue());
-    return new Geometries.SimpleGeom(g);
-  }
-
-  // Geometry properties (2D and 3D) ==========================================
-
-  /** Returns whether {@code geom} has at least one z-coordinate. */
-  public static boolean ST_Is3D(Geom geom) {
-    return geom.g().hasZ();
-  }
-
-  /** Returns the x-value of the first coordinate of {@code geom}. */
-  public static @Nullable Double ST_X(Geom geom) {
-    return geom.g() instanceof Point ? ((Point) geom.g()).getX() : null;
-  }
-
-  /** Returns the y-value of the first coordinate of {@code geom}. */
-  public static @Nullable Double ST_Y(Geom geom) {
-    return geom.g() instanceof Point ? ((Point) geom.g()).getY() : null;
-  }
-
-  /** Returns the z-value of the first coordinate of {@code geom}. */
-  public static @Nullable Double ST_Z(Geom geom) {
-    return geom.g().getDescription().hasZ() && geom.g() instanceof Point
-        ? ((Point) geom.g()).getZ() : null;
-  }
-
-  /** Returns the boundary of {@code geom}. */
-  public static Geom ST_Boundary(Geom geom) {
-    OperatorBoundary op = OperatorBoundary.local();
-    Geometry result = op.execute(geom.g(), null);
-    return geom.wrap(result);
-  }
-
-  /** Returns the distance between {@code geom1} and {@code geom2}. */
-  public static double ST_Distance(Geom geom1, Geom geom2) {
-    return GeometryEngine.distance(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns the type of {@code geom}. */
-  public static String ST_GeometryType(Geom geom) {
-    return Geometries.type(geom.g()).name();
-  }
-
-  /** Returns the OGC SFS type code of {@code geom}. */
-  public static int ST_GeometryTypeCode(Geom geom) {
-    return Geometries.type(geom.g()).code;
-  }
-
-  /** Returns the minimum bounding box of {@code geom} (which may be a
-   *  GEOMETRYCOLLECTION). */
-  public static Geom ST_Envelope(Geom geom) {
-    final Envelope env = envelope(geom.g());
-    return geom.wrap(env);
-  }
-
-  // Geometry predicates ======================================================
-
-  /** Returns whether {@code geom1} contains {@code geom2}. */
-  @Hints({"SqlKind:ST_CONTAINS"})
-  public static boolean ST_Contains(Geom geom1, Geom geom2) {
-    return GeometryEngine.contains(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether {@code geom1} contains {@code geom2} but does not
-   * intersect its boundary. */
-  public static boolean ST_ContainsProperly(Geom geom1, Geom geom2) {
-    return GeometryEngine.contains(geom1.g(), geom2.g(), geom1.sr())
-        && !GeometryEngine.crosses(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether no point in {@code geom2} is outside {@code geom1}. */
-  private static boolean ST_Covers(Geom geom1, Geom geom2)  {
-    throw todo();
-  }
-
-  /** Returns whether {@code geom1} crosses {@code geom2}. */
-  public static boolean ST_Crosses(Geom geom1, Geom geom2)  {
-    return GeometryEngine.crosses(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether {@code geom1} and {@code geom2} are disjoint. */
-  public static boolean ST_Disjoint(Geom geom1, Geom geom2)  {
-    return GeometryEngine.disjoint(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether the envelope of {@code geom1} intersects the envelope of
-   *  {@code geom2}. */
-  public static boolean ST_EnvelopesIntersect(Geom geom1, Geom geom2)  {
-    final Geometry e1 = envelope(geom1.g());
-    final Geometry e2 = envelope(geom2.g());
-    return intersects(e1, e2, geom1.sr());
-  }
-
-  /** Returns whether {@code geom1} equals {@code geom2}. */
-  public static boolean ST_Equals(Geom geom1, Geom geom2)  {
-    return GeometryEngine.equals(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether {@code geom1} intersects {@code geom2}. */
-  public static boolean ST_Intersects(Geom geom1, Geom geom2)  {
-    final Geometry g1 = geom1.g();
-    final Geometry g2 = geom2.g();
-    final SpatialReference sr = geom1.sr();
-    return intersects(g1, g2, sr);
-  }
-
-  /** Returns whether {@code geom1} equals {@code geom2} and their coordinates
-   * and component Geometries are listed in the same order. */
-  public static boolean ST_OrderingEquals(Geom geom1, Geom geom2)  {
-    return GeometryEngine.equals(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns {@code geom1} overlaps {@code geom2}. */
-  public static boolean ST_Overlaps(Geom geom1, Geom geom2)  {
-    return GeometryEngine.overlaps(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether {@code geom1} touches {@code geom2}. */
-  public static boolean ST_Touches(Geom geom1, Geom geom2)  {
-    return GeometryEngine.touches(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether {@code geom1} is within {@code geom2}. */
-  public static boolean ST_Within(Geom geom1, Geom geom2)  {
-    return GeometryEngine.within(geom1.g(), geom2.g(), geom1.sr());
-  }
-
-  /** Returns whether {@code geom1} and {@code geom2} are within
-   * {@code distance} of each other. */
-  @Hints({"SqlKind:ST_DWITHIN"})
-  public static boolean ST_DWithin(Geom geom1, Geom geom2, double distance) {
-    final double distance1 =
-        GeometryEngine.distance(geom1.g(), geom2.g(), geom1.sr());
-    return distance1 <= distance;
-  }
-
-  // Geometry operators (2D and 3D) ===========================================
-
-  /** Computes a buffer around {@code geom}. */
-  public static Geom ST_Buffer(Geom geom, double distance) {
-    final Polygon g = GeometryEngine.buffer(geom.g(), geom.sr(), distance);
-    return geom.wrap(g);
-  }
-
-  /** Computes a buffer around {@code geom} with . */
-  public static Geom ST_Buffer(Geom geom, double distance, int quadSegs) {
-    throw todo();
-  }
-
-  /** Computes a buffer around {@code geom}. */
-  public static Geom ST_Buffer(Geom geom, double bufferSize, String style) {
-    int quadSegCount = 8;
-    CapStyle endCapStyle = CapStyle.ROUND;
-    JoinStyle joinStyle = JoinStyle.ROUND;
-    float mitreLimit = 5f;
-    int i = 0;
-    parse:
-    for (;;) {
-      int equals = style.indexOf('=', i);
-      if (equals < 0) {
-        break;
-      }
-      int space = style.indexOf(' ', equals);
-      if (space < 0) {
-        space = style.length();
-      }
-      String name = style.substring(i, equals);
-      String value = style.substring(equals + 1, space);
-      switch (name) {
-      case "quad_segs":
-        quadSegCount = Integer.parseInt(value);
-        break;
-      case "endcap":
-        endCapStyle = CapStyle.of(value);
-        break;
-      case "join":
-        joinStyle = JoinStyle.of(value);
-        break;
-      case "mitre_limit":
-      case "miter_limit":
-        mitreLimit = Float.parseFloat(value);
-        break;
-      default:
-        // ignore the value
-      }
-      i = space;
-      for (;;) {
-        if (i >= style.length()) {
-          break parse;
-        }
-        if (style.charAt(i) != ' ') {
-          break;
-        }
-        ++i;
-      }
-    }
-    return buffer(geom, bufferSize, quadSegCount, endCapStyle, joinStyle,
-        mitreLimit);
-  }
-
-  /** Computes the union of {@code geom1} and {@code geom2}. */
-  public static Geom ST_Union(Geom geom1, Geom geom2) {
-    SpatialReference sr = geom1.sr();
-    final Geometry g =
-        GeometryEngine.union(new Geometry[]{geom1.g(), geom2.g()}, sr);
-    return bind(g, sr);
-  }
-
-  /** Computes the union of the geometries in {@code geomCollection}. */
-  @SemiStrict public static Geom ST_Union(Geom geomCollection) {
-    SpatialReference sr = geomCollection.sr();
-    final Geometry g =
-        GeometryEngine.union(new Geometry[] {geomCollection.g()}, sr);
-    return bind(g, sr);
-  }
-
-  // Geometry projection functions ============================================
-
-  /** Transforms {@code geom} from one coordinate reference
-   * system (CRS) to the CRS specified by {@code srid}. */
-  public static Geom ST_Transform(Geom geom, int srid) {
-    return geom.transform(srid);
-  }
-
-  /** Returns a copy of {@code geom} with a new SRID. */
-  public static Geom ST_SetSRID(Geom geom, int srid) {
-    return geom.transform(srid);
-  }
-
-  // Space-filling curves
-
-  /** Returns the position of a point on the Hilbert curve,
-   * or null if it is not a 2-dimensional point. */
-  @Hints({"SqlKind:HILBERT"})
-  public static @Nullable Long hilbert(Geom geom) {
-    final Geometry g = geom.g();
-    if (g instanceof Point) {
-      final double x = ((Point) g).getX();
-      final double y = ((Point) g).getY();
-      return new HilbertCurve2D(8).toIndex(x, y);
-    }
-    return null;
-  }
-
-  /** Returns the position of a point on the Hilbert curve. */
-  @Hints({"SqlKind:HILBERT"})
-  public static long hilbert(BigDecimal x, BigDecimal y) {
-    return new HilbertCurve2D(8).toIndex(x.doubleValue(), y.doubleValue());
-  }
-
-  /** Creates a geometry from a WKT.
-   * If the engine returns a null, throws; never returns null. */
-  private static @Nullable Geometry fromWkt(String wkt, int importFlags,
-      Geometry.Type geometryType) {
-    return GeometryEngine.geometryFromWkt(wkt, importFlags, geometryType);
-  }
-
-  // Inner classes ============================================================
-
-  /** Used at run time by the {@link #ST_MakeGrid} and
-   * {@link #ST_MakeGridPoints} functions. */
-  public static class GridEnumerable extends AbstractEnumerable<Object[]> {
-    private final Envelope envelope;
-    private final boolean point;
-    private final double deltaX;
-    private final double deltaY;
-    private final double minX;
-    private final double minY;
-    private final int baseX;
-    private final int baseY;
-    private final int spanX;
-    private final int spanY;
-    private final int area;
-
-    public GridEnumerable(Envelope envelope, BigDecimal deltaX,
-        BigDecimal deltaY, boolean point) {
-      this.envelope = envelope;
-      this.deltaX = deltaX.doubleValue();
-      this.deltaY = deltaY.doubleValue();
-      this.point = point;
-      this.spanX = (int) Math.floor((envelope.getXMax() - envelope.getXMin())
-          / this.deltaX) + 1;
-      this.baseX = (int) Math.floor(envelope.getXMin() / this.deltaX);
-      this.minX = this.deltaX * baseX;
-      this.spanY = (int) Math.floor((envelope.getYMax() - envelope.getYMin())
-          / this.deltaY) + 1;
-      this.baseY = (int) Math.floor(envelope.getYMin() / this.deltaY);
-      this.minY = this.deltaY * baseY;
-      this.area = this.spanX * this.spanY;
-    }
-
-    @Override public Enumerator<Object[]> enumerator() {
-      return new Enumerator<Object[]>() {
-        int id = -1;
-
-        @Override public Object[] current() {
-          final Geom geom;
-          final int x = id % spanX;
-          final int y = id / spanX;
-          if (point) {
-            final double xCurrent = minX + (x + 0.5D) * deltaX;
-            final double yCurrent = minY + (y + 0.5D) * deltaY;
-            geom = ST_MakePoint(BigDecimal.valueOf(xCurrent),
-                BigDecimal.valueOf(yCurrent));
-          } else {
-            final Polygon polygon = new Polygon();
-            final double left = minX + x * deltaX;
-            final double right = left + deltaX;
-            final double bottom = minY + y * deltaY;
-            final double top = bottom + deltaY;
-
-            final Polyline polyline = new Polyline();
-            polyline.addSegment(new Line(left, bottom, right, bottom), true);
-            polyline.addSegment(new Line(right, bottom, right, top), false);
-            polyline.addSegment(new Line(right, top, left, top), false);
-            polyline.addSegment(new Line(left, top, left, bottom), false);
-            polygon.add(polyline, false);
-            geom = new Geometries.SimpleGeom(polygon);
-          }
-          return new Object[] {geom, id, x + 1, y + 1, baseX + x, baseY + y};
-        }
-
-        @Override public boolean moveNext() {
-          return ++id < area;
-        }
-
-        @Override public void reset() {
-          id = -1;
-        }
-
-        @Override public void close() {
-        }
-      };
-    }
-  }
-
-}
diff --git a/core/src/main/java/org/apache/calcite/runtime/Geometries.java 
b/core/src/main/java/org/apache/calcite/runtime/Geometries.java
deleted file mode 100644
index b762ba2f1b..0000000000
--- a/core/src/main/java/org/apache/calcite/runtime/Geometries.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * 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.calcite.runtime;
-
-import org.apache.calcite.linq4j.function.Deterministic;
-import org.apache.calcite.linq4j.function.Experimental;
-import org.apache.calcite.linq4j.function.Strict;
-import org.apache.calcite.util.Util;
-
-import com.esri.core.geometry.Envelope;
-import com.esri.core.geometry.Geometry;
-import com.esri.core.geometry.Line;
-import com.esri.core.geometry.MapGeometry;
-import com.esri.core.geometry.Operator;
-import com.esri.core.geometry.OperatorFactoryLocal;
-import com.esri.core.geometry.OperatorIntersects;
-import com.esri.core.geometry.Point;
-import com.esri.core.geometry.Polyline;
-import com.esri.core.geometry.SpatialReference;
-import com.google.common.collect.ImmutableList;
-
-import java.util.Objects;
-
-/**
- * Utilities for geometry.
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-@Deterministic
-@Strict
-@Experimental
-public class Geometries {
-  static final int NO_SRID = 0;
-  private static final SpatialReference SPATIAL_REFERENCE =
-      SpatialReference.create(4326);
-
-  private Geometries() {}
-
-  static UnsupportedOperationException todo() {
-    return new UnsupportedOperationException();
-  }
-
-  /** Returns a Geom that is a Geometry bound to a SRID. */
-  protected static Geom bind(Geometry geometry, int srid) {
-    if (srid == NO_SRID) {
-      return new SimpleGeom(geometry);
-    }
-    return bind(geometry, SpatialReference.create(srid));
-  }
-
-  static MapGeom bind(Geometry geometry, SpatialReference sr) {
-    return new MapGeom(new MapGeometry(geometry, sr));
-  }
-
-  static Geom makeLine(Geom... geoms) {
-    return makeLine(ImmutableList.copyOf(geoms));
-  }
-
-  public static Geom makeLine(Iterable<? extends Geom> geoms) {
-    final Polyline g = new Polyline();
-    Point p = null;
-    for (Geom geom : geoms) {
-      if (geom.g() instanceof Point) {
-        final Point prev = p;
-        p = (Point) geom.g();
-        if (prev != null) {
-          final Line line = new Line();
-          line.setStart(prev);
-          line.setEnd(p);
-          g.addSegment(line, false);
-        }
-      }
-    }
-    return new SimpleGeom(g);
-  }
-
-  static Geom point(double x, double y) {
-    final Geometry g = new Point(x, y);
-    return new SimpleGeom(g);
-  }
-
-  /** Returns the OGC type of a geometry. */
-  public static Type type(Geometry g) {
-    switch (g.getType()) {
-    case Point:
-      return Type.POINT;
-    case Polyline:
-      return Type.LINESTRING;
-    case Polygon:
-      return Type.POLYGON;
-    case MultiPoint:
-      return Type.MULTIPOINT;
-    case Envelope:
-      return Type.POLYGON;
-    case Line:
-      return Type.LINESTRING;
-    case Unknown:
-      return Type.Geometry;
-    default:
-      throw new AssertionError(g);
-    }
-  }
-
-  static Envelope envelope(Geometry g) {
-    final Envelope env = new Envelope();
-    g.queryEnvelope(env);
-    return env;
-  }
-
-  static boolean intersects(Geometry g1, Geometry g2,
-      SpatialReference sr) {
-    final OperatorIntersects op = (OperatorIntersects) OperatorFactoryLocal
-        .getInstance().getOperator(Operator.Type.Intersects);
-    return op.execute(g1, g2, sr, null);
-  }
-
-  static Geom buffer(Geom geom, double bufferSize,
-      int quadSegCount, CapStyle endCapStyle, JoinStyle joinStyle,
-      float mitreLimit) {
-    Util.discard(endCapStyle + ":" + joinStyle + ":" + mitreLimit
-        + ":" + quadSegCount);
-    throw todo();
-  }
-
-  /** How the "buffer" command terminates the end of a line. */
-  enum CapStyle {
-    ROUND, FLAT, SQUARE;
-
-    static CapStyle of(String value) {
-      switch (value) {
-      case "round":
-        return ROUND;
-      case "flat":
-      case "butt":
-        return FLAT;
-      case "square":
-        return SQUARE;
-      default:
-        throw new IllegalArgumentException("unknown endcap value: " + value);
-      }
-    }
-  }
-
-  /** How the "buffer" command decorates junctions between line segments. */
-  enum JoinStyle {
-    ROUND, MITRE, BEVEL;
-
-    static JoinStyle of(String value) {
-      switch (value) {
-      case "round":
-        return ROUND;
-      case "mitre":
-      case "miter":
-        return MITRE;
-      case "bevel":
-        return BEVEL;
-      default:
-        throw new IllegalArgumentException("unknown join value: " + value);
-      }
-    }
-  }
-
-  /** Geometry types, with the names and codes assigned by OGC. */
-  public enum Type {
-    Geometry(0),
-    POINT(1),
-    LINESTRING(2),
-    POLYGON(3),
-    MULTIPOINT(4),
-    MULTILINESTRING(5),
-    MULTIPOLYGON(6),
-    GEOMCOLLECTION(7),
-    CURVE(13),
-    SURFACE(14),
-    POLYHEDRALSURFACE(15);
-
-    final int code;
-
-    Type(int code) {
-      this.code = code;
-    }
-  }
-
-  /** Geometry. It may or may not have a spatial reference
-   * associated with it. */
-  public interface Geom extends Comparable<Geom> {
-    Geometry g();
-
-    Type type();
-
-    SpatialReference sr();
-
-    Geom transform(int srid);
-
-    Geom wrap(Geometry g);
-  }
-
-  /** Sub-class of geometry that has no spatial reference. */
-  static class SimpleGeom implements Geom {
-    final Geometry g;
-
-    SimpleGeom(Geometry g) {
-      this.g = Objects.requireNonNull(g, "g");
-    }
-
-    @Override public String toString() {
-      return g.toString();
-    }
-
-    @Override public int compareTo(Geom o) {
-      return toString().compareTo(o.toString());
-    }
-
-    @Override public Geometry g() {
-      return g;
-    }
-
-    @Override public Type type() {
-      return Geometries.type(g);
-    }
-
-    @Override public SpatialReference sr() {
-      return SPATIAL_REFERENCE;
-    }
-
-    @Override public Geom transform(int srid) {
-      if (srid == SPATIAL_REFERENCE.getID()) {
-        return this;
-      }
-      return bind(g, srid);
-    }
-
-    @Override public Geom wrap(Geometry g) {
-      return new SimpleGeom(g);
-    }
-  }
-
-  /** Sub-class of geometry that has a spatial reference. */
-  static class MapGeom implements Geom {
-    final MapGeometry mg;
-
-    MapGeom(MapGeometry mg) {
-      this.mg = Objects.requireNonNull(mg, "mg");
-    }
-
-    @Override public String toString() {
-      return mg.toString();
-    }
-
-    @Override public int compareTo(Geom o) {
-      return toString().compareTo(o.toString());
-    }
-
-    @Override public Geometry g() {
-      return mg.getGeometry();
-    }
-
-    @Override public Type type() {
-      return Geometries.type(mg.getGeometry());
-    }
-
-    @Override public SpatialReference sr() {
-      return mg.getSpatialReference();
-    }
-
-    @Override public Geom transform(int srid) {
-      if (srid == NO_SRID) {
-        return new SimpleGeom(mg.getGeometry());
-      }
-      if (srid == mg.getSpatialReference().getID()) {
-        return this;
-      }
-      return bind(Objects.requireNonNull(mg.getGeometry()), srid);
-    }
-
-    @Override public Geom wrap(Geometry g) {
-      return bind(g, this.mg.getSpatialReference());
-    }
-  }
-}
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/ProjectionTransformer.java 
b/core/src/main/java/org/apache/calcite/runtime/ProjectionTransformer.java
new file mode 100644
index 0000000000..825d5e7de1
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/ProjectionTransformer.java
@@ -0,0 +1,166 @@
+/*
+ * 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.calcite.runtime;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryCollection;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.MultiLineString;
+import org.locationtech.jts.geom.MultiPoint;
+import org.locationtech.jts.geom.MultiPolygon;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
+import org.locationtech.jts.geom.util.GeometryTransformer;
+import org.locationtech.proj4j.CRSFactory;
+import org.locationtech.proj4j.CoordinateReferenceSystem;
+import org.locationtech.proj4j.CoordinateTransform;
+import org.locationtech.proj4j.CoordinateTransformFactory;
+import org.locationtech.proj4j.ProjCoordinate;
+
+import java.util.Locale;
+import java.util.stream.Stream;
+
+/**
+ * Transforms the projection of a geometry.
+ */
+public class ProjectionTransformer extends GeometryTransformer {
+
+  private final CoordinateTransform coordinateTransform;
+
+  /**
+   * Creates a transformer that reprojects geometries with the provided SRIDs.
+   *
+   * @param sourceSrid the source SRID
+   * @param targetSrid the target SRID
+   */
+  public ProjectionTransformer(int sourceSrid, int targetSrid) {
+    CRSFactory crsFactory = new CRSFactory();
+    CoordinateReferenceSystem source = crsFactory
+        .createFromName(String.format(Locale.ROOT, "epsg:%s", sourceSrid));
+    CoordinateReferenceSystem target = crsFactory
+        .createFromName(String.format(Locale.ROOT, "epsg:%s", targetSrid));
+    CoordinateTransformFactory ctFactory = new CoordinateTransformFactory();
+    coordinateTransform = ctFactory.createTransform(source, target);
+  }
+
+  @Override protected CoordinateSequence transformCoordinates(
+      CoordinateSequence coordinateSequence, Geometry parent) {
+    Coordinate[] coordinateArray =
+        Stream.of(coordinateSequence.toCoordinateArray())
+            .map(this::transformCoordinate)
+            .toArray(Coordinate[]::new);
+    return new CoordinateArraySequence(coordinateArray);
+  }
+
+  private Coordinate transformCoordinate(Coordinate coordinate) {
+    ProjCoordinate c1 = new ProjCoordinate(coordinate.x, coordinate.y);
+    ProjCoordinate c2 = coordinateTransform.transform(c1, new 
ProjCoordinate());
+    return new Coordinate(c2.x, c2.y);
+  }
+
+  @Override protected Geometry transformPoint(Point geom, Geometry parent) {
+    try {
+      Geometry geometry = super.transformPoint(geom, parent);
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  @Override protected Geometry transformMultiPoint(MultiPoint geom, Geometry 
parent) {
+    try {
+      Geometry geometry = super.transformMultiPoint(geom, parent);
+      if (geometry instanceof Point) {
+        geometry = factory.createMultiPoint(new Point[]{(Point) geometry});
+      }
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  @Override protected Geometry transformLinearRing(LinearRing geom, Geometry 
parent) {
+    try {
+      Geometry geometry = super.transformLinearRing(geom, parent);
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  @Override protected Geometry transformLineString(LineString geom, Geometry 
parent) {
+    try {
+      Geometry geometry = super.transformLineString(geom, parent);
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  @Override protected Geometry transformMultiLineString(MultiLineString geom, 
Geometry parent) {
+    try {
+      Geometry geometry = super.transformMultiLineString(geom, parent);
+      if (geometry instanceof LineString) {
+        geometry = factory.createMultiLineString(new LineString[]{(LineString) 
geometry});
+      }
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  @Override protected Geometry transformPolygon(Polygon geom, Geometry parent) 
{
+    try {
+      Geometry geometry = super.transformPolygon(geom, parent);
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  @Override protected Geometry transformMultiPolygon(MultiPolygon geom, 
Geometry parent) {
+    try {
+      Geometry geometry = super.transformMultiPolygon(geom, parent);
+      if (geometry instanceof Polygon) {
+        geometry = factory.createMultiPolygon(new Polygon[]{(Polygon) 
geometry});
+      }
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  @Override protected Geometry transformGeometryCollection(
+      GeometryCollection geom, Geometry parent) {
+    try {
+      Geometry geometry = super.transformGeometryCollection(geom, parent);
+      return withTargetSRID(geometry);
+    } catch (Exception e) {
+      return parent.getFactory().createEmpty(0);
+    }
+  }
+
+  private Geometry withTargetSRID(Geometry outputGeom) {
+    int srid = 
coordinateTransform.getTargetCRS().getProjection().getEPSGCode();
+    outputGeom.setSRID(srid);
+    return outputGeom;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/SpatialTypeFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SpatialTypeFunctions.java
new file mode 100644
index 0000000000..d7808a475f
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/SpatialTypeFunctions.java
@@ -0,0 +1,553 @@
+/*
+ * 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.calcite.runtime;
+
+import org.apache.calcite.linq4j.AbstractEnumerable;
+import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.linq4j.function.Deterministic;
+import org.apache.calcite.linq4j.function.Experimental;
+import org.apache.calcite.linq4j.function.Hints;
+import org.apache.calcite.linq4j.function.SemiStrict;
+import org.apache.calcite.linq4j.function.Strict;
+import org.apache.calcite.runtime.SpatialTypeUtils.SpatialType;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+import static org.apache.calcite.runtime.SpatialTypeUtils.GEOMETRY_FACTORY;
+import static org.apache.calcite.runtime.SpatialTypeUtils.NO_SRID;
+import static org.apache.calcite.runtime.SpatialTypeUtils.asEwkt;
+import static org.apache.calcite.runtime.SpatialTypeUtils.asWkt;
+import static org.apache.calcite.runtime.SpatialTypeUtils.fromWkt;
+
+/**
+ * Helper methods to implement spatial type (ST) functions in generated code.
+ *
+ * <p>Remaining tasks:
+ *
+ * <ul>
+ *   <li>Determine type code for
+ *   {@link org.apache.calcite.sql.type.ExtraSqlTypes#GEOMETRY}
+ *   <li>Should we create aliases for functions in upper-case?
+ *   Without ST_ prefix?
+ *   <li>Consider adding spatial literals, e.g. `GEOMETRY 'POINT (30 10)'`
+ *   <li>Integer arguments, e.g. SELECT ST_MakePoint(1, 2, 1.5),
+ *     ST_MakePoint(1, 2)
+ *   <li>Are GEOMETRY values comparable? If so add ORDER BY test
+ *   <li>We have to add 'Z' to create 3D objects. This is inconsistent with
+ *   PostGIS. Who is right? At least document the difference.
+ *   <li>Should add GeometryEngine.intersects; similar to disjoint etc.
+ *   <li>Make {@link #ST_MakeLine(Geometry, Geometry)} varargs</li>
+ * </ul>
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@Deterministic
+@Strict
+@Experimental
+public class SpatialTypeFunctions {
+
+  private SpatialTypeFunctions() {}
+
+  // Geometry conversion functions (2D and 3D) ================================
+
+  public static @Nullable String ST_AsEWKT(Geometry g) {
+    return asEwkt(g);
+  }
+
+  public static @Nullable String ST_AsText(Geometry g) {
+    return asWkt(g);
+  }
+
+  public static @Nullable String ST_AsWKT(Geometry g) {
+    return asWkt(g);
+  }
+
+  public static @Nullable Geometry ST_GeomFromText(String s) {
+    return fromWkt(s);
+  }
+
+  public static @Nullable Geometry ST_GeomFromText(String s, int srid) {
+    final Geometry g = fromWkt(s);
+    g.setSRID(srid);
+    return g;
+  }
+
+  public static @Nullable Geometry ST_LineFromText(String s) {
+    return ST_GeomFromText(s, NO_SRID);
+  }
+
+  public static @Nullable Geometry ST_LineFromText(String wkt, int srid) {
+    final Geometry g = fromWkt(wkt);
+    g.setSRID(srid);
+    return g == null ? null : g;
+  }
+
+  public static @Nullable Geometry ST_MPointFromText(String s) {
+    return ST_GeomFromText(s, NO_SRID);
+  }
+
+  public static @Nullable Geometry ST_MPointFromText(String wkt, int srid) {
+    final Geometry g = fromWkt(wkt);
+    g.setSRID(srid);
+    return g == null ? null : g;
+  }
+
+  public static @Nullable Geometry ST_PointFromText(String s) {
+    return ST_GeomFromText(s, NO_SRID);
+  }
+
+  public static @Nullable Geometry ST_PointFromText(String wkt, int srid) {
+    final Geometry g = fromWkt(wkt);
+    g.setSRID(srid);
+    return g == null ? null : g;
+  }
+
+  public static @Nullable Geometry ST_PolyFromText(String s) {
+    return ST_GeomFromText(s, NO_SRID);
+  }
+
+  public static @Nullable Geometry ST_PolyFromText(String wkt, int srid) {
+    final Geometry g = fromWkt(wkt);
+    g.setSRID(srid);
+    return g == null ? null : g;
+  }
+
+  public static @Nullable Geometry ST_MLineFromText(String s) {
+    return ST_GeomFromText(s, NO_SRID);
+  }
+
+  public static @Nullable Geometry ST_MLineFromText(String wkt, int srid) {
+    final Geometry g = fromWkt(wkt);
+    g.setSRID(srid);
+    return g == null ? null : g;
+  }
+
+  public static @Nullable Geometry ST_MPolyFromText(String s) {
+    return ST_GeomFromText(s, NO_SRID);
+  }
+
+  public static @Nullable Geometry ST_MPolyFromText(String wkt, int srid) {
+    final Geometry g = fromWkt(wkt);
+    g.setSRID(srid);
+    return g == null ? null : g;
+  }
+
+  // Geometry creation functions ==============================================
+
+  /** Calculates a regular grid of polygons based on {@code geom}. */
+  private static void ST_MakeGrid(final Geometry geom,
+      final BigDecimal deltaX, final BigDecimal deltaY) {
+    // This is a dummy function. We cannot include table functions in this
+    // package, because they have too many dependencies. See the real 
definition
+    // in SqlSpatialTypeFunctions.
+  }
+
+  /** Calculates a regular grid of points based on {@code geom}. */
+  private static void ST_MakeGridPoints(final Geometry geom,
+      final BigDecimal deltaX, final BigDecimal deltaY) {
+    // This is a dummy function. We cannot include table functions in this
+    // package, because they have too many dependencies. See the real 
definition
+    // in SqlSpatialTypeFunctions.
+  }
+
+  /** Creates a rectangular Polygon. */
+  public static Geometry ST_MakeEnvelope(BigDecimal xMin, BigDecimal yMin,
+      BigDecimal xMax, BigDecimal yMax, int srid) {
+    Geometry geom = ST_GeomFromText("POLYGON(("
+        + xMin + " " + yMin + ", "
+        + xMin + " " + yMax + ", "
+        + xMax + " " + yMax + ", "
+        + xMax + " " + yMin + ", "
+        + xMin + " " + yMin + "))", srid);
+    return Objects.requireNonNull(geom, "geom");
+  }
+
+  /** Creates a rectangular Polygon. */
+  public static Geometry ST_MakeEnvelope(BigDecimal xMin, BigDecimal yMin,
+      BigDecimal xMax, BigDecimal yMax) {
+    return ST_MakeEnvelope(xMin, yMin, xMax, yMax, NO_SRID);
+  }
+
+  /** Creates a line-string from the given POINTs (or MULTIPOINTs). */
+  @Hints({"SqlKind:ST_MAKE_LINE"})
+  public static Geometry ST_MakeLine(Geometry geom1, Geometry geom2) {
+    return GEOMETRY_FACTORY.createLineString(new Coordinate[] {
+        geom1.getCoordinate(),
+        geom2.getCoordinate(),
+    });
+  }
+
+  @Hints({"SqlKind:ST_MAKE_LINE"})
+  public static Geometry ST_MakeLine(Geometry geom1, Geometry geom2, Geometry 
geom3) {
+    return GEOMETRY_FACTORY.createLineString(new Coordinate[] {
+        geom1.getCoordinate(),
+        geom2.getCoordinate(),
+        geom3.getCoordinate(),
+    });
+  }
+
+  @Hints({"SqlKind:ST_MAKE_LINE"})
+  public static Geometry ST_MakeLine(Geometry geom1, Geometry geom2, Geometry 
geom3,
+      Geometry geom4) {
+    return GEOMETRY_FACTORY.createLineString(new Coordinate[] {
+        geom1.getCoordinate(),
+        geom2.getCoordinate(),
+        geom3.getCoordinate(),
+        geom4.getCoordinate(),
+    });
+  }
+
+  @Hints({"SqlKind:ST_MAKE_LINE"})
+  public static Geometry ST_MakeLine(Geometry geom1, Geometry geom2, Geometry 
geom3,
+      Geometry geom4, Geometry geom5) {
+    return GEOMETRY_FACTORY.createLineString(new Coordinate[] {
+        geom1.getCoordinate(),
+        geom2.getCoordinate(),
+        geom3.getCoordinate(),
+        geom4.getCoordinate(),
+        geom5.getCoordinate(),
+    });
+  }
+
+  @Hints({"SqlKind:ST_MAKE_LINE"})
+  public static Geometry ST_MakeLine(Geometry geom1, Geometry geom2, Geometry 
geom3,
+      Geometry geom4, Geometry geom5, Geometry geom6) {
+    return GEOMETRY_FACTORY.createLineString(new Coordinate[] {
+        geom1.getCoordinate(),
+        geom2.getCoordinate(),
+        geom3.getCoordinate(),
+        geom4.getCoordinate(),
+        geom5.getCoordinate(),
+        geom6.getCoordinate(),
+    });
+  }
+
+  /** Alias for {@link #ST_Point(BigDecimal, BigDecimal)}. */
+  @Hints({"SqlKind:ST_POINT"})
+  public static Geometry ST_MakePoint(BigDecimal x, BigDecimal y) {
+    return ST_Point(x, y);
+  }
+
+  /** Alias for {@link #ST_Point(BigDecimal, BigDecimal, BigDecimal)}. */
+  @Hints({"SqlKind:ST_POINT3"})
+  public static Geometry ST_MakePoint(BigDecimal x, BigDecimal y, BigDecimal 
z) {
+    return ST_Point(x, y, z);
+  }
+
+  /** Constructs a 2D point from coordinates. */
+  @Hints({"SqlKind:ST_POINT"})
+  public static Geometry ST_Point(BigDecimal x, BigDecimal y) {
+    // NOTE: Combine the double and BigDecimal variants of this function
+    return GEOMETRY_FACTORY.createPoint(new Coordinate(x.doubleValue(), 
y.doubleValue()));
+  }
+
+  /** Constructs a 3D point from coordinates. */
+  @Hints({"SqlKind:ST_POINT3"})
+  public static Geometry ST_Point(BigDecimal x, BigDecimal y, BigDecimal z) {
+    final Geometry g = GEOMETRY_FACTORY.createPoint(
+        new Coordinate(x.doubleValue(), y.doubleValue(),
+        z.doubleValue()));
+    return g;
+  }
+
+  // Geometry properties (2D and 3D) ==========================================
+
+  /** Returns whether {@code geom} has at least one z-coordinate. */
+  public static boolean ST_Is3D(Geometry geom) {
+    for (Coordinate coordinate : geom.getCoordinates()) {
+      if (!Double.isNaN(coordinate.getZ())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /** Returns the x-value of the first coordinate of {@code geom}. */
+  public static @Nullable Double ST_X(Geometry geom) {
+    return geom instanceof Point ? ((Point) geom).getX() : null;
+  }
+
+  /** Returns the y-value of the first coordinate of {@code geom}. */
+  public static @Nullable Double ST_Y(Geometry geom) {
+    return geom instanceof Point ? ((Point) geom).getY() : null;
+  }
+
+  /** Returns the z-value of the first coordinate of {@code geom}. */
+  public static @Nullable Double ST_Z(Geometry geom) {
+    return geom instanceof Point
+        && !Double.isNaN(geom.getCoordinate().getZ())
+        ? geom.getCoordinate().getZ() : null;
+  }
+
+  /** Returns the boundary of {@code geom}. */
+  public static Geometry ST_Boundary(Geometry geom) {
+    return geom.getBoundary();
+  }
+
+  /** Returns the distance between {@code geom1} and {@code geom2}. */
+  public static double ST_Distance(Geometry geom1, Geometry geom2) {
+    return geom1.distance(geom2);
+  }
+
+  /** Returns the type of {@code geom}. */
+  public static String ST_GeometryType(Geometry geom) {
+    return SpatialType.fromGeometry(geom).name();
+  }
+
+  /** Returns the OGC SFS type code of {@code geom}. */
+  public static int ST_GeometryTypeCode(Geometry geom) {
+    return SpatialType.fromGeometry(geom).code();
+  }
+
+  /** Returns the minimum bounding box of {@code geom} (which may be a
+   *  GEOMETRYCOLLECTION). */
+  public static Geometry ST_Envelope(Geometry geom) {
+    return geom.getEnvelope();
+  }
+
+  // Geometry predicates ======================================================
+
+  /** Returns whether {@code geom1} contains {@code geom2}. */
+  @Hints({"SqlKind:ST_CONTAINS"})
+  public static boolean ST_Contains(Geometry geom1, Geometry geom2) {
+    return geom1.contains(geom2);
+  }
+
+  /** Returns whether {@code geom1} contains {@code geom2} but does not
+   * intersect its boundary. */
+  public static boolean ST_ContainsProperly(Geometry geom1, Geometry geom2) {
+    return geom1.contains(geom2)
+        && !geom1.crosses(geom2);
+  }
+
+  /** Returns whether no point in {@code geom2} is outside {@code geom1}. */
+  private static boolean ST_Covers(Geometry geom1, Geometry geom2)  {
+    return geom1.covers(geom2);
+  }
+
+  /** Returns whether {@code geom1} crosses {@code geom2}. */
+  public static boolean ST_Crosses(Geometry geom1, Geometry geom2)  {
+    return geom1.crosses(geom2);
+  }
+
+  /** Returns whether {@code geom1} and {@code geom2} are disjoint. */
+  public static boolean ST_Disjoint(Geometry geom1, Geometry geom2)  {
+    return geom1.disjoint(geom2);
+  }
+
+  /** Returns whether the envelope of {@code geom1} intersects the envelope of
+   *  {@code geom2}. */
+  public static boolean ST_EnvelopesIntersect(Geometry geom1, Geometry geom2)  
{
+    final Geometry e1 = geom1.getEnvelope();
+    final Geometry e2 = geom2.getEnvelope();
+    return e1.intersects(e2);
+  }
+
+  /** Returns whether {@code geom1} equals {@code geom2}. */
+  public static boolean ST_Equals(Geometry geom1, Geometry geom2)  {
+    return geom1.equals(geom2);
+  }
+
+  /** Returns whether {@code geom1} intersects {@code geom2}. */
+  public static boolean ST_Intersects(Geometry geom1, Geometry geom2)  {
+    return geom1.intersects(geom2);
+  }
+
+  /** Returns whether {@code geom1} equals {@code geom2} and their coordinates
+   * and component Geometries are listed in the same order. */
+  public static boolean ST_OrderingEquals(Geometry geom1, Geometry geom2)  {
+    return geom1.equals(geom2);
+  }
+
+  /** Returns {@code geom1} overlaps {@code geom2}. */
+  public static boolean ST_Overlaps(Geometry geom1, Geometry geom2)  {
+    return geom1.overlaps(geom2);
+  }
+
+  /** Returns whether {@code geom1} touches {@code geom2}. */
+  public static boolean ST_Touches(Geometry geom1, Geometry geom2)  {
+    return geom1.touches(geom2);
+  }
+
+  /** Returns whether {@code geom1} is within {@code geom2}. */
+  public static boolean ST_Within(Geometry geom1, Geometry geom2)  {
+    return geom1.within(geom2);
+  }
+
+  /** Returns whether {@code geom1} and {@code geom2} are within
+   * {@code distance} of each other. */
+  @Hints({"SqlKind:ST_DWITHIN"})
+  public static boolean ST_DWithin(Geometry geom1, Geometry geom2, double 
distance) {
+    final double distance1 = geom1.distance(geom2);
+    return distance1 <= distance;
+  }
+
+  // Geometry operators (2D and 3D) ===========================================
+
+  /** Computes a buffer around {@code geom}. */
+  public static Geometry ST_Buffer(Geometry geom, double distance) {
+    return geom.buffer(distance);
+  }
+
+  /** Computes a buffer around {@code geom}. */
+  public static Geometry ST_Buffer(Geometry geom, double distance, int 
quadSegs) {
+    return geom.buffer(distance, quadSegs);
+  }
+
+  /** Computes a buffer around {@code geom}. */
+  public static Geometry ST_Buffer(Geometry geom, double distance, int 
quadSegs, int endCapStyle) {
+    return geom.buffer(distance, quadSegs, endCapStyle);
+  }
+
+  /** Computes the union of {@code geom1} and {@code geom2}. */
+  public static Geometry ST_Union(Geometry geom1, Geometry geom2) {
+    return geom1.union(geom2);
+  }
+
+  /** Computes the union of the geometries in {@code geomCollection}. */
+  @SemiStrict public static Geometry ST_Union(Geometry geomCollection) {
+    return geomCollection.union();
+  }
+
+  // Geometry projection functions ============================================
+
+  /** Transforms {@code geom} from one coordinate reference
+   * system (CRS) to the CRS specified by {@code srid}. */
+  public static Geometry ST_Transform(Geometry geom, int srid) {
+    ProjectionTransformer projectionTransformer =
+        new ProjectionTransformer(geom.getSRID(), srid);
+    return projectionTransformer.transform(geom);
+  }
+
+  /** Returns a copy of {@code geom} with a new SRID. */
+  public static Geometry ST_SetSRID(Geometry geom, int srid) {
+    geom.setSRID(srid);
+    return geom;
+  }
+
+  // Space-filling curves
+
+  /** Returns the position of a point on the Hilbert curve,
+   * or null if it is not a 2-dimensional point. */
+  @Hints({"SqlKind:HILBERT"})
+  public static @Nullable Long hilbert(Geometry geom) {
+    if (geom instanceof Point) {
+      final double x = ((Point) geom).getX();
+      final double y = ((Point) geom).getY();
+      return new HilbertCurve2D(8).toIndex(x, y);
+    }
+    return null;
+  }
+
+  /** Returns the position of a point on the Hilbert curve. */
+  @Hints({"SqlKind:HILBERT"})
+  public static long hilbert(BigDecimal x, BigDecimal y) {
+    return new HilbertCurve2D(8).toIndex(x.doubleValue(), y.doubleValue());
+  }
+
+  // Inner classes ============================================================
+
+  /** Used at run time by the {@link #ST_MakeGrid} and
+   * {@link #ST_MakeGridPoints} functions. */
+  public static class GridEnumerable extends AbstractEnumerable<Object[]> {
+    private final Envelope envelope;
+    private final boolean point;
+    private final double deltaX;
+    private final double deltaY;
+    private final double minX;
+    private final double minY;
+    private final int baseX;
+    private final int baseY;
+    private final int spanX;
+    private final int spanY;
+    private final int area;
+
+    public GridEnumerable(Envelope envelope, BigDecimal deltaX,
+        BigDecimal deltaY, boolean point) {
+      this.envelope = envelope;
+      this.deltaX = deltaX.doubleValue();
+      this.deltaY = deltaY.doubleValue();
+      this.point = point;
+      this.spanX = (int) Math.floor((envelope.getMaxX() - envelope.getMinX())
+          / this.deltaX) + 1;
+      this.baseX = (int) Math.floor(envelope.getMinX() / this.deltaX);
+      this.minX = this.deltaX * baseX;
+      this.spanY = (int) Math.floor((envelope.getMaxY() - envelope.getMinY())
+          / this.deltaY) + 1;
+      this.baseY = (int) Math.floor(envelope.getMinY() / this.deltaY);
+      this.minY = this.deltaY * baseY;
+      this.area = this.spanX * this.spanY;
+    }
+
+    @Override public Enumerator<Object[]> enumerator() {
+      return new Enumerator<Object[]>() {
+        int id = -1;
+
+        @Override public Object[] current() {
+          final Geometry geom;
+          final int x = id % spanX;
+          final int y = id / spanX;
+          if (point) {
+            final double xCurrent = minX + (x + 0.5D) * deltaX;
+            final double yCurrent = minY + (y + 0.5D) * deltaY;
+            geom = ST_MakePoint(BigDecimal.valueOf(xCurrent),
+                BigDecimal.valueOf(yCurrent));
+          } else {
+            final double left = minX + x * deltaX;
+            final double right = left + deltaX;
+            final double bottom = minY + y * deltaY;
+            final double top = bottom + deltaY;
+
+            Coordinate[] coordinates = new Coordinate[] {
+                new Coordinate(left, bottom),
+                new Coordinate(left, top),
+                new Coordinate(right, top),
+                new Coordinate(right, bottom),
+                new Coordinate(left, bottom)
+            };
+
+            LinearRing linearRing = 
GEOMETRY_FACTORY.createLinearRing(coordinates);
+            Polygon polygon = GEOMETRY_FACTORY.createPolygon(linearRing);
+
+            geom = polygon;
+          }
+          return new Object[] {geom, id, x + 1, y + 1, baseX + x, baseY + y};
+        }
+
+        @Override public boolean moveNext() {
+          return ++id < area;
+        }
+
+        @Override public void reset() {
+          id = -1;
+        }
+
+        @Override public void close() {
+        }
+      };
+    }
+  }
+
+}
diff --git 
a/core/src/main/java/org/apache/calcite/runtime/SpatialTypeUtils.java 
b/core/src/main/java/org/apache/calcite/runtime/SpatialTypeUtils.java
new file mode 100644
index 0000000000..7828083203
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/runtime/SpatialTypeUtils.java
@@ -0,0 +1,225 @@
+/*
+ * 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.calcite.runtime;
+
+import org.apache.calcite.linq4j.function.Deterministic;
+import org.apache.calcite.linq4j.function.Experimental;
+import org.apache.calcite.linq4j.function.Strict;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.io.ParseException;
+import org.locationtech.jts.io.WKBReader;
+import org.locationtech.jts.io.WKBWriter;
+import org.locationtech.jts.io.WKTReader;
+import org.locationtech.jts.io.WKTWriter;
+import org.locationtech.jts.io.geojson.GeoJsonReader;
+import org.locationtech.jts.io.geojson.GeoJsonWriter;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for spatial types.
+ */
+@Deterministic
+@Strict
+@Experimental
+public class SpatialTypeUtils {
+  static final int NO_SRID = 0;
+
+  public static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+  private SpatialTypeUtils() {}
+
+  /** Geometry types, with the names and codes assigned by OGC. */
+  public enum SpatialType {
+    GEOMETRY(0),
+    POINT(1),
+    LINESTRING(2),
+    POLYGON(3),
+    MULTIPOINT(4),
+    MULTILINESTRING(5),
+    MULTIPOLYGON(6),
+    GEOMETRYCOLLECTION(7);
+
+    private final int code;
+
+    SpatialType(int code) {
+      this.code = code;
+    }
+
+    public int code() {
+      return code;
+    }
+
+    /** Returns the OGC type of a geometry. */
+    public static SpatialType fromGeometry(Geometry g) {
+      switch (g.getGeometryType()) {
+      case "Geometry":
+        return SpatialType.GEOMETRY;
+      case "Point":
+        return SpatialType.POINT;
+      case "LineString":
+        return SpatialType.LINESTRING;
+      case "Polygon":
+        return SpatialType.POLYGON;
+      case "MultiPoint":
+        return SpatialType.MULTIPOINT;
+      case "MultiLineString":
+        return SpatialType.MULTILINESTRING;
+      case "MultiPolygon":
+        return SpatialType.MULTIPOLYGON;
+      case "GeometryCollection":
+        return SpatialType.GEOMETRYCOLLECTION;
+      default:
+        throw new AssertionError(g);
+      }
+    }
+  }
+
+  private static int dimension(Geometry geometry) {
+    int dimension = 3;
+    for (Coordinate coordinate : geometry.getCoordinates()) {
+      if (Double.isNaN(coordinate.getZ())) {
+        dimension = 2;
+        break;
+      }
+    }
+    return dimension;
+  }
+
+  /**
+   * Constructs a geometry from a GeoJson representation.
+   *
+   * @param geoJson a GeoJson
+   * @return a geometry
+   */
+  public static Geometry fromGeoJson(String geoJson) {
+    try {
+      GeoJsonReader reader = new GeoJsonReader();
+      return reader.read(geoJson);
+    } catch (ParseException e) {
+      throw new RuntimeException("Unable to parse GeoJSON");
+    }
+  }
+
+  /**
+   * Constructs a geometry from a Well-Known binary (WKB) representation.
+   *
+   * @param wkb a WKB
+   * @return a geometry
+   */
+  public static Geometry fromWkb(byte[] wkb) {
+    try {
+      WKBReader reader = new WKBReader();
+      return reader.read(wkb);
+    } catch (ParseException e) {
+      throw new RuntimeException("Unable to parse WKB");
+    }
+  }
+
+  /**
+   * Constructs a geometry from an Extended Well-Known text (EWKT) 
representation.
+   * EWKT representations are prefixed with the SRID.
+   *
+   * @param ewkt an EWKT
+   * @return a geometry
+   */
+  public static Geometry fromEwkt(String ewkt) {
+    Pattern pattern = Pattern.compile("^(?:srid:(\\d*);)?(.*)$");
+    java.util.regex.Matcher matcher = pattern.matcher(ewkt);
+    if (!matcher.matches()) {
+      throw new RuntimeException("Unable to parse EWKT");
+    }
+
+    String wkt = matcher.group(2);
+    if (wkt == null) {
+      throw new RuntimeException("Unable to parse EWKT");
+    }
+
+    Geometry geometry = fromWkt(wkt);
+    String srid = matcher.group(1);
+    if (srid != null) {
+      geometry.setSRID(Integer.parseInt(srid));
+    }
+
+    return geometry;
+  }
+
+  /**
+   * Constructs a geometry from a Well-Known text (WKT) representation.
+   *
+   * @param wkt a WKT
+   * @return a geometry
+   */
+  public static Geometry fromWkt(String wkt) {
+    try {
+      WKTReader reader = new WKTReader();
+      return reader.read(wkt);
+    } catch (ParseException e) {
+      throw new RuntimeException("Unable to parse WKT");
+    }
+  }
+
+  /**
+   * Returns the GeoJson representation of the geometry.
+   *
+   * @param geometry a geometry
+   * @return a GeoJson
+   */
+  public static String asGeoJson(Geometry geometry) {
+    GeoJsonWriter geoJsonWriter = new GeoJsonWriter();
+    return geoJsonWriter.write(geometry);
+  }
+
+  /**
+   * Returns the Extended Well-Known binary (WKB) representation of the 
geometry.
+   *
+   * @param geometry a geometry
+   * @return an WKB
+   */
+  public static byte[] asWkb(Geometry geometry) {
+    int outputDimension = dimension(geometry);
+    WKBWriter wkbWriter = new WKBWriter(outputDimension);
+    return wkbWriter.write(geometry);
+  }
+
+  /**
+   * Returns the Extended Well-Known text (EWKT) representation of the 
geometry.
+   * EWKT representations are prefixed with the SRID.
+   *
+   * @param geometry a geometry
+   * @return an EWKT
+   */
+  public static String asEwkt(Geometry geometry) {
+    return String.format(Locale.ROOT, "srid:%s;%s", geometry.getSRID(), 
asWkt(geometry));
+  }
+
+  /**
+   * Returns the Well-Known text (WKT) representation of the geometry.
+   *
+   * @param geometry a geometry
+   * @return a WKT
+   */
+  public static String asWkt(Geometry geometry) {
+    int outputDimension = dimension(geometry);
+    WKTWriter wktWriter = new WKTWriter(outputDimension);
+    return wktWriter.write(geometry);
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlGeoFunctions.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlSpatialTypeFunctions.java
similarity index 84%
rename from core/src/main/java/org/apache/calcite/sql/fun/SqlGeoFunctions.java
rename to 
core/src/main/java/org/apache/calcite/sql/fun/SqlSpatialTypeFunctions.java
index 08206aabeb..2094afe166 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlGeoFunctions.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlSpatialTypeFunctions.java
@@ -22,8 +22,7 @@ import org.apache.calcite.linq4j.Enumerable;
 import org.apache.calcite.linq4j.Linq4j;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.runtime.GeoFunctions;
-import org.apache.calcite.runtime.Geometries.Geom;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.Statistic;
@@ -33,39 +32,38 @@ import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.ImmutableBitSet;
 
-import com.esri.core.geometry.Envelope;
-import com.esri.core.geometry.Geometry;
 import com.google.common.collect.ImmutableList;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
+import org.locationtech.jts.geom.Geometry;
 
 import java.math.BigDecimal;
 
 /**
- * Utilities for Geo/Spatial functions.
+ * Utilities for spatial type functions.
  *
  * <p>Includes some table functions, and may in future include other functions
  * that have dependencies beyond the {@code org.apache.calcite.runtime} 
package.
  */
-public class SqlGeoFunctions {
-  private SqlGeoFunctions() {}
+public class SqlSpatialTypeFunctions {
+  private SqlSpatialTypeFunctions() {}
 
   // Geometry table functions =================================================
 
   /** Calculates a regular grid of polygons based on {@code geom}.
    *
-   * @see GeoFunctions ST_MakeGrid */
+   * @see SpatialTypeFunctions ST_MakeGrid */
   @SuppressWarnings({"WeakerAccess", "unused"})
-  public static ScannableTable ST_MakeGrid(final Geom geom,
+  public static ScannableTable ST_MakeGrid(final Geometry geom,
       final BigDecimal deltaX, final BigDecimal deltaY) {
     return new GridTable(geom, deltaX, deltaY, false);
   }
 
   /** Calculates a regular grid of points based on {@code geom}.
    *
-   * @see GeoFunctions ST_MakeGridPoints */
+   * @see SpatialTypeFunctions ST_MakeGridPoints */
   @SuppressWarnings({"WeakerAccess", "unused"})
-  public static ScannableTable ST_MakeGridPoints(final Geom geom,
+  public static ScannableTable ST_MakeGridPoints(final Geometry geom,
       final BigDecimal deltaX, final BigDecimal deltaY) {
     return new GridTable(geom, deltaX, deltaY, true);
   }
@@ -73,12 +71,12 @@ public class SqlGeoFunctions {
   /** Returns the points or rectangles in a grid that covers a given
    * geometry. */
   public static class GridTable implements ScannableTable {
-    private final Geom geom;
+    private final Geometry geom;
     private final BigDecimal deltaX;
     private final BigDecimal deltaY;
     private boolean point;
 
-    GridTable(Geom geom, BigDecimal deltaX, BigDecimal deltaY,
+    GridTable(Geometry geom, BigDecimal deltaX, BigDecimal deltaY,
         boolean point) {
       this.geom = geom;
       this.deltaX = deltaX;
@@ -105,12 +103,9 @@ public class SqlGeoFunctions {
 
     @Override public Enumerable<@Nullable Object[]> scan(DataContext root) {
       if (geom != null && deltaX != null && deltaY != null) {
-        final Geometry geometry = geom.g();
-        final Envelope envelope = new Envelope();
-        geometry.queryEnvelope(envelope);
         if (deltaX.compareTo(BigDecimal.ZERO) > 0
             && deltaY.compareTo(BigDecimal.ZERO) > 0) {
-          return new GeoFunctions.GridEnumerable(envelope, deltaX, deltaY,
+          return new 
SpatialTypeFunctions.GridEnumerable(geom.getEnvelopeInternal(), deltaX, deltaY,
               point);
         }
       }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
 
b/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
index 4b211d3e00..f1941b29fe 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/type/JavaToSqlTypeConversionRules.java
@@ -17,11 +17,11 @@
 package org.apache.calcite.sql.type;
 
 import org.apache.calcite.avatica.util.ArrayImpl;
-import org.apache.calcite.runtime.Geometries;
 
 import com.google.common.collect.ImmutableMap;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
+import org.locationtech.jts.geom.Geometry;
 
 import java.math.BigDecimal;
 import java.sql.Date;
@@ -73,7 +73,7 @@ public class JavaToSqlTypeConversionRules {
           .put(Time.class, SqlTypeName.TIME)
           .put(BigDecimal.class, SqlTypeName.DECIMAL)
 
-          .put(Geometries.Geom.class, SqlTypeName.GEOMETRY)
+          .put(Geometry.class, SqlTypeName.GEOMETRY)
 
           .put(ResultSet.class, SqlTypeName.CURSOR)
           .put(ColumnList.class, SqlTypeName.COLUMN_LIST)
diff --git 
a/core/src/main/java/org/apache/calcite/sql/util/SqlOperatorTables.java 
b/core/src/main/java/org/apache/calcite/sql/util/SqlOperatorTables.java
index 3392a7bafe..bd16425718 100644
--- a/core/src/main/java/org/apache/calcite/sql/util/SqlOperatorTables.java
+++ b/core/src/main/java/org/apache/calcite/sql/util/SqlOperatorTables.java
@@ -17,10 +17,10 @@
 package org.apache.calcite.sql.util;
 
 import org.apache.calcite.prepare.CalciteCatalogReader;
-import org.apache.calcite.runtime.GeoFunctions;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlOperatorTable;
-import org.apache.calcite.sql.fun.SqlGeoFunctions;
+import org.apache.calcite.sql.fun.SqlSpatialTypeFunctions;
 
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
@@ -41,8 +41,8 @@ public class SqlOperatorTables {
 
   private static SqlOperatorTable createSpatial() {
     return CalciteCatalogReader.operatorTable(
-        GeoFunctions.class.getName(),
-        SqlGeoFunctions.class.getName());
+        SpatialTypeFunctions.class.getName(),
+        SqlSpatialTypeFunctions.class.getName());
   }
 
   /** Returns the Spatial operator table, creating it if necessary. */
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index e1089a8fb0..a3e11dba27 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -87,13 +87,13 @@ import org.apache.calcite.runtime.CompressionFunctions;
 import org.apache.calcite.runtime.Enumerables;
 import org.apache.calcite.runtime.FlatLists;
 import org.apache.calcite.runtime.FunctionContexts;
-import org.apache.calcite.runtime.GeoFunctions;
 import org.apache.calcite.runtime.JsonFunctions;
 import org.apache.calcite.runtime.Matcher;
 import org.apache.calcite.runtime.Pattern;
 import org.apache.calcite.runtime.RandomFunction;
 import org.apache.calcite.runtime.ResultSetEnumerable;
 import org.apache.calcite.runtime.SortedMultiMap;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.runtime.SqlFunctions.FlatProductInputType;
 import org.apache.calcite.runtime.Utilities;
@@ -391,7 +391,7 @@ public enum BuiltInMethod {
   IS_JSON_OBJECT(JsonFunctions.class, "isJsonObject", String.class),
   IS_JSON_ARRAY(JsonFunctions.class, "isJsonArray", String.class),
   IS_JSON_SCALAR(JsonFunctions.class, "isJsonScalar", String.class),
-  ST_GEOM_FROM_TEXT(GeoFunctions.class, "ST_GeomFromText", String.class),
+  ST_GEOM_FROM_TEXT(SpatialTypeFunctions.class, "ST_GeomFromText", 
String.class),
   INITCAP(SqlFunctions.class, "initcap", String.class),
   SUBSTRING(SqlFunctions.class, "substring", String.class, int.class,
       int.class),
diff --git 
a/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java 
b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java
index 8694369542..267aa39ca1 100644
--- 
a/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java
+++ 
b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java
@@ -21,7 +21,7 @@ import org.apache.calcite.linq4j.tree.Expression;
 import org.apache.calcite.linq4j.tree.Expressions;
 import org.apache.calcite.linq4j.tree.MethodCallExpression;
 import org.apache.calcite.linq4j.tree.ParameterExpression;
-import org.apache.calcite.runtime.GeoFunctions;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.runtime.XmlFunctions;
 import org.apache.calcite.util.BuiltInMethod;
@@ -205,10 +205,10 @@ public final class EnumUtilsTest {
     final ConstantExpression arg4 = Expressions.constant(1, int.class);
     final ConstantExpression arg5 = Expressions.constant(2, int.class);
     final MethodCallExpression geoMethodCall =
-        EnumUtils.call(null, GeoFunctions.class, "ST_MakePoint",
+        EnumUtils.call(null, SpatialTypeFunctions.class, "ST_MakePoint",
             Arrays.asList(arg4, arg5));
     assertThat(Expressions.toString(geoMethodCall),
-        is("org.apache.calcite.runtime.GeoFunctions.ST_MakePoint("
+        is("org.apache.calcite.runtime.SpatialTypeFunctions.ST_MakePoint("
             + "new java.math.BigDecimal(\n  1), "
             + "new java.math.BigDecimal(\n  2))"));
   }
diff --git 
a/core/src/test/java/org/apache/calcite/runtime/SpatialTypeUtilsTest.java 
b/core/src/test/java/org/apache/calcite/runtime/SpatialTypeUtilsTest.java
new file mode 100644
index 0000000000..4cae60670f
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/runtime/SpatialTypeUtilsTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.calcite.runtime;
+
+import org.junit.jupiter.api.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests {@link org.apache.calcite.runtime.SpatialTypeUtilsTest}.
+ */
+class SpatialTypeUtilsTest {
+
+  @Test void testFromEwkt() {
+    Geometry g1 = SpatialTypeUtils.fromEwkt("POINT(1 2)");
+    assertEquals(1, g1.getCoordinate().getX());
+    assertEquals(2, g1.getCoordinate().getY());
+
+    Geometry g2 = SpatialTypeUtils.fromEwkt("srid:1234;POINT(1 2)");
+    assertEquals(1234, g2.getSRID());
+    assertEquals(1, g2.getCoordinate().getX());
+    assertEquals(2, g2.getCoordinate().getY());
+  }
+
+  @Test void testAsEwkt() {
+    GeometryFactory gf = new GeometryFactory();
+    Geometry g1 = gf.createPoint(new Coordinate(1, 2));
+    g1.setSRID(1234);
+    assertEquals("srid:1234;POINT (1 2)", SpatialTypeUtils.asEwkt(g1));
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 2ed221d69d..7aa4790955 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -6977,12 +6977,12 @@ public class JdbcTest {
    * Enable spatial operator table by adding 'fun=spatial'to JDBC URL</a>. */
   @Test void testFunSpatial() {
     final String sql = "select distinct\n"
-        + "  ST_PointFromText('POINT(-71.0642.28)') as c\n"
+        + "  ST_PointFromText('POINT(-71.0642 .28)') as c\n"
         + "from \"hr\".\"emps\"";
     CalciteAssert.that(CalciteAssert.Config.REGULAR)
         .with(CalciteConnectionProperty.FUN, "spatial")
         .query(sql)
-        .returnsUnordered("C={\"x\":-71.0642,\"y\":0.28}");
+        .returnsUnordered("C=POINT (-71.0642 0.28)");
 
     // NVL is present in the Oracle operator table, but not spatial or core
     CalciteAssert.that(CalciteAssert.Config.REGULAR)
@@ -7733,9 +7733,9 @@ public class JdbcTest {
         .planContains("static final java.math.BigDecimal 
$L4J$C$new_java_math_BigDecimal_1_ = "
             + "new java.math.BigDecimal(\n"
             + "              1)")
-        .planContains("org.apache.calcite.runtime.GeoFunctions.ST_MakePoint("
+        
.planContains("org.apache.calcite.runtime.SpatialTypeFunctions.ST_MakePoint("
             + "$L4J$C$new_java_math_BigDecimal_1_, literal_value0)")
-        .returns("EXPR$0={\"x\":1,\"y\":2.1}\n");
+        .returns("EXPR$0=POINT (1 2.1)\n");
   }
 
   @Test void testMatchSimple() {
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java 
b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index a5cdf6c0a2..606c63cb54 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -6548,7 +6548,7 @@ class RelOptRulesTest extends RelOptTestBase {
   /** Constant reduction on geo-spatial expression. */
   @Test void testSpatialReduce() {
     final String sql = "select\n"
-        + "  ST_Buffer(ST_Point(0.0, 1.0), 2) as b\n"
+        + "  ST_Buffer(ST_Point(0.0, 0.0), 1, 4) as b\n"
         + "from GEO.Restaurants as r";
     spatial(sql)
         .withRelBuilderSimplify(false)
diff --git 
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml 
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 5426348933..6b12293717 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -13089,7 +13089,7 @@ LogicalProject(NAME=[$0], LATITUDE=[$1], 
LONGITUDE=[$2], CUISINE=[$3], HILBERT=[
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(NAME=[$0], LATITUDE=[$1], LONGITUDE=[$2], CUISINE=[$3], 
HILBERT=[$4])
-  LogicalFilter(condition=[AND(SEARCH($4, Sarg[[33033..33035], [33050..33053], 
[33055..33080], [33092..33100], [33112..33156], [33164..33176], [33236..33244], 
[33252..33254], [36456..36464], [36496..36520]]), ST_CONTAINS(MULTIPOLYGON 
(((16 20, 15.987153539431617 20.39241877538086, 15.948669168242859 
20.78315715332031, 15.884711682419379 21.17054193209677, 15.795554957734407 
21.552914270615126, 15.68158077697063 21.92863679181897, 15.543277195067716 
22.296100594190538, 15.381236449196127  [...]
+  LogicalFilter(condition=[AND(SEARCH($4, Sarg[[33033..33035], [33050..33053], 
[33055..33080], [33092..33100], [33112..33156], [33164..33176], [33236..33244], 
[33252..33254], [36456..36464], [36496..36520]]), ST_CONTAINS(POLYGON ((16 20, 
15.884711682419383 18.829458067903232, 15.54327719506772 17.703899405809462, 
14.988817673815271 16.666578601882385, 14.242640687119286 15.757359312880716, 
13.333421398117615 15.011182326184729, 12.29610059419054 14.45672280493228, 
11.17054193209677 14.11 [...]
     LogicalTableScan(table=[[CATALOG, GEO, RESTAURANTS]])
 ]]>
     </Resource>
@@ -13112,7 +13112,7 @@ LogicalProject(NAME=[$0], LATITUDE=[$1], 
LONGITUDE=[$2], CUISINE=[$3], HILBERT=[
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(NAME=[$0], LATITUDE=[$1], LONGITUDE=[$2], CUISINE=[$3], 
HILBERT=[$4])
-  LogicalFilter(condition=[AND(SEARCH($4, Sarg[[33064..33076], [33092..33100], 
[33112..33156], [33164..33176], [33236..33240], [36457..36459], [36501..36503], 
[36505..36507], [36517..36519]]), ST_DWITHIN(MULTILINESTRING ((8 20, 12 
20)):GEOMETRY, ST_POINT($2, $1), 4))])
+  LogicalFilter(condition=[AND(SEARCH($4, Sarg[[33064..33076], [33092..33100], 
[33112..33156], [33164..33176], [33236..33240], [36457..36459], [36501..36503], 
[36505..36507], [36517..36519]]), ST_DWITHIN(LINESTRING (8 20, 12 20):GEOMETRY, 
ST_POINT($2, $1), 4))])
     LogicalTableScan(table=[[CATALOG, GEO, RESTAURANTS]])
 ]]>
     </Resource>
@@ -13207,18 +13207,18 @@ LogicalProject(NAME=[$0], LATITUDE=[$1], 
LONGITUDE=[$2], CUISINE=[$3], HILBERT=[
   <TestCase name="testSpatialReduce">
     <Resource name="sql">
       <![CDATA[select
-  ST_Buffer(ST_Point(0.0, 1.0), 2) as b
+  ST_Buffer(ST_Point(0.0, 0.0), 1, 4) as b
 from GEO.Restaurants as r]]>
     </Resource>
     <Resource name="planBefore">
       <![CDATA[
-LogicalProject(B=[ST_BUFFER(ST_POINT(0.0:DECIMAL(2, 1), 1.0:DECIMAL(2, 1)), 
2)])
+LogicalProject(B=[ST_BUFFER(ST_POINT(0.0:DECIMAL(2, 1), 0.0:DECIMAL(2, 1)), 1, 
4)])
   LogicalTableScan(table=[[CATALOG, GEO, RESTAURANTS]])
 ]]>
     </Resource>
     <Resource name="planAfter">
       <![CDATA[
-LogicalProject(B=[CAST(MULTIPOLYGON (((2 1, 1.9957178464772056 
1.1308062584602858, 1.9828897227476194 1.2610523844401027, 1.9615705608064595 
1.3901806440322562, 1.9318516525781353 1.517638090205041, 1.8938602589902103 
1.6428789306063227, 1.8477590650225726 1.7653668647301788, 1.7937454830653756 
1.8845773804380017, 1.7320508075688765 1.9999999999999991, 1.6629392246050898 
2.1111404660392035, 1.5867066805824699 2.217522858017441, 1.5036796149579543 
2.318691630200137, 1.4142135623730947 2.4 [...]
+LogicalProject(B=[CAST(POLYGON ((1 0, 0.9238795325112867 -0.3826834323650898, 
0.7071067811865476 -0.7071067811865475, 0.3826834323650898 -0.9238795325112867, 
0.0000000000000001 -1, -0.3826834323650897 -0.9238795325112867, 
-0.7071067811865475 -0.7071067811865476, -0.9238795325112867 
-0.3826834323650899, -1 -0.0000000000000001, -0.9238795325112868 
0.3826834323650897, -0.7071067811865477 0.7071067811865475, -0.3826834323650903 
0.9238795325112865, -0.0000000000000002 1, 0.38268343236509 0.92 [...]
   LogicalTableScan(table=[[CATALOG, GEO, RESTAURANTS]])
 ]]>
     </Resource>
diff --git a/core/src/test/resources/sql/spatial.iq 
b/core/src/test/resources/sql/spatial.iq
index 1dfc45efe1..dfafb93e2c 100644
--- a/core/src/test/resources/sql/spatial.iq
+++ b/core/src/test/resources/sql/spatial.iq
@@ -80,28 +80,28 @@ null
 
 SELECT ST_GeomFromText('LINESTRING(-71.160281 42.258729,-71.160837 
42.259113,-71.161144 42.25932)');
 EXPR$0
-{"paths":[[[-71.160281,42.258729],[-71.160837,42.259113],[-71.161144,42.25932]]]}
+LINESTRING (-71.160281 42.258729, -71.160837 42.259113, -71.161144 42.25932)
 !ok
 
 SELECT ST_GeomFromText('LINESTRING(-71.160281 42.258729,-71.160837 
42.259113,-71.161144 42.25932)',4269);
 EXPR$0
-{"paths":[[[-71.160281,42.258729],[-71.160837,42.259113],[-71.161144,42.25932]]],"spatialReference":{"wkid":4269}}
+LINESTRING (-71.160281 42.258729, -71.160837 42.259113, -71.161144 42.25932)
 !ok
 
 SELECT ST_GeomFromText('MULTILINESTRING((-71.160281 42.258729,-71.160837 
42.259113,-71.161144 42.25932))');
 EXPR$0
-{"paths":[[[-71.160281,42.258729],[-71.160837,42.259113],[-71.161144,42.25932]]]}
+MULTILINESTRING ((-71.160281 42.258729, -71.160837 42.259113, -71.161144 
42.25932))
 !ok
 
 SELECT ST_GeomFromText('POINT(-71.064544 42.28787)');
 EXPR$0
-{"x":-71.064544,"y":42.28787}
+POINT (-71.064544 42.28787)
 !ok
 
 SELECT ST_GeomFromText('POLYGON((-71.1776585052917 
42.3902909739571,-71.1776820268866 42.3903701743239,
 -71.1776063012595 42.3903825660754,-71.1775826583081 
42.3903033653531,-71.1776585052917 42.3902909739571))');
 EXPR$0
-{"rings":[[[-71.1776585052917,42.3902909739571],[-71.1776820268866,42.3903701743239],[-71.1776063012595,42.3903825660754],[-71.1775826583081,42.3903033653531],[-71.1776585052917,42.3902909739571]]]}
+POLYGON ((-71.1776585052917 42.3902909739571, -71.1776820268866 
42.3903701743239, -71.1776063012595 42.3903825660754, -71.1775826583081 
42.3903033653531, -71.1776585052917 42.3902909739571))
 !ok
 
 SELECT ST_GeomFromText('MULTIPOLYGON(((-71.1031880899493 42.3152774590236,
@@ -128,7 +128,7 @@ SELECT ST_GeomFromText('MULTIPOLYGON(((-71.1031880899493 
42.3152774590236,
 ((-71.1043632495873 42.315113108546,-71.1043583974082 42.3151211109857,
 -71.1043443253471 42.3150676015829,-71.1043850704575 
42.3150793250568,-71.1043632495873 42.315113108546)))',4326);
 EXPR$0
-{"rings":[[[-71.1031880899493,42.3152774590236],[-71.1031627617667,42.3152960829043],[-71.102923838298,42.3149156848307],[-71.1023097974109,42.3151969047397],[-71.1019285062273,42.3147384934248],[-...
 (1697 characters)
+MULTIPOLYGON (((-71.1031880899493 42.3152774590236, -71.1031627617667 
42.3152960829043, -71.102923838298 42.3149156848307, -71.1023097974109 
42.3151969047397, -71.1019285062273 42.3147384934248, -71.102505233663 
42.3144722937587, -71.10277487471 42.3141658254797, -71.103113945163 
42.3142739188902, -71.10324876416 42.31402489987, -71.1033002961013 
42.3140393340215, -71.1033488797549 42.3139495090772, -71.103396240451 
42.3138632439557, -71.1041521907712 42.3141153348029, -71.1041411411543  [...]
 !ok
 
 # Disabled: Should not return null
@@ -152,7 +152,7 @@ SELECT ST_GeomFromText('CIRCULARSTRING(220268 150415,220227 
150505,220227 150406
 # but ST_GeomFromText('GEOMETRYCOLLECTION EMPTY') is not preferred.
 SELECT ST_GeomFromText('GEOMETRYCOLLECTION EMPTY');
 EXPR$0
-null
+GEOMETRYCOLLECTION EMPTY
 !ok
 
 # ST_GeomFromWKB(wkb [, srid ]) Well Known Binary to Geometry
@@ -165,7 +165,7 @@ null
 SELECT ST_LineFromText('LINESTRING(1 2, 3 4)') AS aline,
   ST_LineFromText('POINT(1 2)') AS null_return;
 ALINE, NULL_RETURN
-{"paths":[[[1,2],[3,4]]]}, {"x":1,"y":2}
+LINESTRING (1 2, 3 4), POINT (1 2)
 !ok
 
 # ST_LineFromWKB(wkb [, srid ]) Well Known Binary to LINESTRING
@@ -174,30 +174,30 @@ ALINE, NULL_RETURN
 # ST_MLineFromText(wkt [, srid ]) Well Known Text to MULTILINESTRING
 SELECT ST_MLineFromText('MULTILINESTRING((1 2, 3 4), (4 5, 6 7))');
 EXPR$0
-{"paths":[[[1,2],[3,4]],[[4,5],[6,7]]]}
+MULTILINESTRING ((1 2, 3 4), (4 5, 6 7))
 !ok
 
 # ST_MPointFromText(wkt [, srid ]) Well Known Text to MULTIPOINT
 SELECT ST_MPointFromText('MULTIPOINT(1 2, 3 4)');
 EXPR$0
-{"points":[[1,2],[3,4]]}
+MULTIPOINT ((1 2), (3 4))
 !ok
 
-SELECT ST_MPointFromText('MULTIPOINT(-70.9590 42.1180, -70.9611 42.1223)', 
4326);
+SELECT ST_AsEWKT(ST_MPointFromText('MULTIPOINT(-70.9590 42.1180, -70.9611 
42.1223)', 4326));
 EXPR$0
-{"points":[[-70.959,42.118],[-70.9611,42.1223]],"spatialReference":{"wkid":4326}}
+srid:4326;MULTIPOINT ((-70.959 42.118), (-70.9611 42.1223))
 !ok
 
 # ST_MPolyFromText(wkt [, srid ]) Well Known Text to MULTIPOLYGON
-SELECT ST_MPolyFromText('MULTIPOLYGON Z(((0 0 1,20 0 1,20 20 1,0 20 1,0 0 
1),(5 5 3,5 7 3,7 7 3,7 5 3,5 5 3)))');
+SELECT ST_AsWKT(ST_MPolyFromText('MULTIPOLYGON Z(((0 0 1,20 0 1,20 20 1,0 20 
1,0 0 1),(5 5 3,5 7 3,7 7 3,7 5 3,5 5 3)))'));
 EXPR$0
-{"hasZ":true,"rings":[[[0,0,1],[0,20,1],[20,20,1],[20,0,1],[0,0,1]],[[5,5,3],[7,5,3],[7,7,3],[5,7,3],[5,5,3]]]}
+MULTIPOLYGON Z(((0 0 1, 20 0 1, 20 20 1, 0 20 1, 0 0 1), (5 5 3, 5 7 3, 7 7 3, 
7 5 3, 5 5 3)))
 !ok
 
-SELECt ST_MPolyFromText('MULTIPOLYGON(((-70.916 42.1002,-70.9468 
42.0946,-70.9765 42.0872,-70.9754 42.0875,-70.9749 42.0879,-70.9752 
42.0881,-70.9754 42.0891,-70.9758 42.0894,-70.9759 42.0897,-70.9759 
42.0899,-70.9754 42.0902,-70.9756 42.0906,-70.9753 42.0907,-70.9753 
42.0917,-70.9757 42.0924,-70.9755 42.0928,-70.9755 42.0942,-70.9751 
42.0948,-70.9755 42.0953,-70.9751 42.0958,-70.9751 42.0962,-70.9759 
42.0983,-70.9767 42.0987,-70.9768 42.0991,-70.9771 42.0997,-70.9771 
42.1003,-70.9768 42 [...]
+SELECT ST_MPolyFromText('MULTIPOLYGON(((-70.916 42.1002,-70.9468 
42.0946,-70.9765 42.0872,-70.9754 42.0875,-70.9749 42.0879,-70.9752 
42.0881,-70.9754 42.0891,-70.9758 42.0894,-70.9759 42.0897,-70.9759 
42.0899,-70.9754 42.0902,-70.9756 42.0906,-70.9753 42.0907,-70.9753 
42.0917,-70.9757 42.0924,-70.9755 42.0928,-70.9755 42.0942,-70.9751 
42.0948,-70.9755 42.0953,-70.9751 42.0958,-70.9751 42.0962,-70.9759 
42.0983,-70.9767 42.0987,-70.9768 42.0991,-70.9771 42.0997,-70.9771 
42.1003,-70.9768 42 [...]
     -70.9408 42.1513,-70.9315 42.1165,-70.916 42.1002)))',4326);
 EXPR$0
-{"rings":[[[-70.916,42.1002],[-70.9468,42.0946],[-70.9765,42.0872],[-70.9754,42.0875],[-70.9749,42.0879],[-70.9752,42.0881],[-70.9754,42.0891],[-70.9758,42.0894],[-70.9759,42.0897],[-70.9759,42.089...
 (1123 characters)
+MULTIPOLYGON (((-70.916 42.1002, -70.9468 42.0946, -70.9765 42.0872, -70.9754 
42.0875, -70.9749 42.0879, -70.9752 42.0881, -70.9754 42.0891, -70.9758 
42.0894, -70.9759 42.0897, -70.9759 42.0899, -70.9754 42.0902, -70.9756 
42.0906, -70.9753 42.0907, -70.9753 42.0917, -70.9757 42.0924, -70.9755 
42.0928, -70.9755 42.0942, -70.9751 42.0948, -70.9755 42.0953, -70.9751 
42.0958, -70.9751 42.0962, -70.9759 42.0983, -70.9767 42.0987, -70.9768 
42.0991, -70.9771 42.0997, -70.9771 42.1003, -70.9768  [...]
 !ok
 
 # ST_OSMMapLink(geom [, marker ]) Geometry to OSM map link
@@ -206,21 +206,21 @@ EXPR$0
 # ST_PointFromText(wkt [, srid ]) Well Known Text to POINT
 SELECT ST_PointFromText('POINT(-71.064544 42.28787)');
 EXPR$0
-{"x":-71.064544,"y":42.28787}
+POINT (-71.064544 42.28787)
 !ok
 
-SELECT ST_PointFromText('POINT(-71.064544 42.28787)', 4326);
+SELECT ST_AsEWKT(ST_PointFromText('POINT(-71.064544 42.28787)', 4326));
 EXPR$0
-{"x":-71.064544,"y":42.28787,"spatialReference":{"wkid":4326}}
+srid:4326;POINT (-71.064544 42.28787)
 !ok
 
 # ST_PointFromWKB(wkb [, srid ]) Well Known Binary to POINT
 # Not implemented
 
 # ST_PolyFromText(wkt [, srid ]) Well Known Text to POLYGON
-SELECT ST_PolyFromText('POLYGON Z((0 0 1,20 0 1,20 20 1,0 20 1,0 0 1))');
+SELECT ST_AsWKT(ST_PolyFromText('POLYGON Z((0 0 1,20 0 1,20 20 1,0 20 1,0 0 
1))'));
 EXPR$0
-{"hasZ":true,"rings":[[[0,0,1],[0,20,1],[20,20,1],[20,0,1],[0,0,1]]]}
+POLYGON Z((0 0 1, 20 0 1, 20 20 1, 0 20 1, 0 0 1))
 !ok
 
 SELECT ST_PolyFromText(CAST(NULL AS VARCHAR));
@@ -228,9 +228,9 @@ EXPR$0
 null
 !ok
 
-SELECT ST_PolyFromText('POLYGON((0 0))');
+SELECT ST_PolyFromText('POLYGON((0 0, 0 1, 1 1, 0 0))');
 EXPR$0
-{"rings":[[[0,0],[0,0]]]}
+POLYGON ((0 0, 0 1, 1 1, 0 0))
 !ok
 
 # ST_PolyFromWKB(wkb [, srid ]) Well Known Binary to POLYGON
@@ -268,25 +268,25 @@ EXPR$0
 SELECT ST_AsText(ST_MakeEnvelope(10.0, 10.0, 11.0, 11.0, 4326));
 
 EXPR$0
-MULTIPOLYGON (((10 10, 11 10, 11 11, 10 11, 10 10)))
+POLYGON ((10 10, 10 11, 11 11, 11 10, 10 10))
 !ok
 
 SELECT ST_AsText(ST_MakeEnvelope(12.0, -1.0, 6.0, 4.0, 4326));
 
 EXPR$0
-MULTIPOLYGON (((12 -1, 12 4, 6 4, 6 -1, 12 -1)))
+POLYGON ((12 -1, 12 4, 6 4, 6 -1, 12 -1))
 !ok
 
 # ST_MakeGrid(geom, deltaX, deltaY) Calculates a regular grid of polygons 
based on *geom*
 SELECT * FROM TABLE(ST_MakeGrid(ST_Point(13.0,22.0), 10.0, 5.0));
 THE_GEOM, ID, ID_COL, ID_ROW, ABS_COL, ABS_ROW
-{"rings":[[[10,20],[20,20],[20,25],[10,25],[10,20]]]}, 0, 1, 1, 1, 4
+POLYGON ((10 20, 10 25, 20 25, 20 20, 10 20)), 0, 1, 1, 1, 4
 !ok
 
 # ST_MakeGridPoints(geom, deltaX, deltaY) Calculates a regular grid of points 
based on *geom*
 SELECT * FROM TABLE(ST_MakeGridPoints(ST_Point(13.0,22.0), 10.0, 5.0));
 THE_GEOM, ID, ID_COL, ID_ROW, ABS_COL, ABS_ROW
-{"x":15,"y":22.5}, 0, 1, 1, 1, 4
+POINT (15 22.5), 0, 1, 1, 1, 4
 !ok
 
 # Call ST_MakeGridPoints for each geometry in a set
@@ -296,21 +296,21 @@ from GEO."countries" AS c,
     ST_MakeGridPoints(ST_MakePoint("longitude", "latitude"), 10.0, 10.0)) as p
 ORDER BY "latitude" DESC LIMIT 3;
 name, latitude, longitude, THE_GEOM, ID, ID_COL, ID_ROW, ABS_COL, ABS_ROW
-Svalbard and Jan Mayen, 77.553604, 23.670272, {"x":25,"y":75}, 0, 1, 1, 2, 7
-Greenland, 71.706936, -42.604303, {"x":-45,"y":75}, 0, 1, 1, -5, 7
-Iceland, 64.963051, -19.020835, {"x":-15,"y":65}, 0, 1, 1, -2, 6
+Svalbard and Jan Mayen, 77.553604, 23.670272, POINT (25 75), 0, 1, 1, 2, 7
+Greenland, 71.706936, -42.604303, POINT (-45 75), 0, 1, 1, -5, 7
+Iceland, 64.963051, -19.020835, POINT (-15 65), 0, 1, 1, -2, 6
 !ok
 
 # ST_MakeLine(point1 [, point ]*) Creates a line-string from the given points 
(or multi-points)
 
 SELECT ST_MakeLine(ST_Point(1.0,1.0), ST_Point(-1.0,-1.0));
 EXPR$0
-{"paths":[[[1,1],[-1,-1]]]}
+LINESTRING (1 1, -1 -1)
 !ok
 
 SELECT ST_MakeLine(ST_Point(1.0,1.0), ST_Point(-1.0,-1.0), ST_Point(-3.0,0.0));
 EXPR$0
-{"paths":[[[1,1],[-1,-1],[-3,0]]]}
+LINESTRING (1 1, -1 -1, -3 0)
 !ok
 
 # ST_MakePoint(x, y [, z ]) Constructs a point from two or three coordinates
@@ -318,19 +318,19 @@ EXPR$0
 # Return point with unknown SRID
 SELECT ST_MakePoint(-71.1043443253471, 42.3150676015829);
 EXPR$0
-{"x":-71.1043443253471,"y":42.3150676015829}
+POINT (-71.1043443253471 42.3150676015829)
 !ok
 
 # Return point marked as WGS 84 long lat
 SELECT ST_SetSRID(ST_MakePoint(-71.1043443253471, 42.3150676015829),4326);
 EXPR$0
-{"x":-71.1043443253471,"y":42.3150676015829}
+POINT (-71.1043443253471 42.3150676015829)
 !ok
 
 # Return a 3D point (e.g. has altitude)
-SELECT ST_MakePoint(1.0, 2.0, 1.5);
+SELECT ST_AsWKT(ST_MakePoint(1.0, 2.0, 1.5));
 EXPR$0
-{"x":1,"y":2,"z":1.5}
+POINT Z(1 2 1.5)
 !ok
 
 # Get x of point
@@ -356,8 +356,8 @@ from GEO."countries" AS c
 ORDER BY "latitude" DESC LIMIT 3;
 name, P
 U.S.Minor Outlying Islands, null
-Svalbard and Jan Mayen, {"x":23.670272,"y":77.553604}
-Greenland, {"x":-42.604303,"y":71.706936}
+Svalbard and Jan Mayen, POINT (23.670272 77.553604)
+Greenland, POINT (-42.604303 71.706936)
 !ok
 
 # ST_MakePolygon(lineString [, hole ]*) Creates a polygon from *lineString* 
with the given holes (which are required to be closed line-strings)
@@ -396,21 +396,19 @@ MULTIPOINT ((1 1), (-1 1))
 
 SELECT ST_AsText(ST_Boundary(ST_GeomFromText('POLYGON((1 1,0 0, -1 1, 1 
1))')));
 EXPR$0
-MULTILINESTRING ((1 1, 0 0, -1 1, 1 1))
+LINEARRING (1 1, 0 0, -1 1, 1 1)
 !ok
 
 # Using a 3d polygon
 SELECT ST_AsText(ST_Boundary(ST_GeomFromText('POLYGON Z((1 1 1,0 0 1, -1 1 1, 
1 1 1))')));
-
 EXPR$0
-MULTILINESTRING Z ((1 1 1, 0 0 1, -1 1 1, 1 1 1))
+LINEARRING Z(1 1 1, 0 0 1, -1 1 1, 1 1 1)
 !ok
 
 # Using a 3d multilinestring
 SELECT ST_AsText(ST_Boundary(ST_GeomFromText('MULTILINESTRING Z((1 1 1,0 0 
0.5, -1 1 1),(1 1 0.5,0 0 0.5, -1 1 0.5, 1 1 0.5) )')));
-
 EXPR$0
-MULTIPOINT Z ((1 1 1), (-1 1 1))
+MULTIPOINT Z((-1 1 1), (1 1 1))
 !ok
 
 # ST_Centroid(geom) Returns the centroid of *geom* (which may be a 
geometry-collection)
@@ -435,35 +433,35 @@ EXPR$0
 !ok
 
 SELECT ST_Distance(
-    ST_GeomFromText('POINT(10 10)',4326),
+    ST_GeomFromText('POINT(10 10)', 4326),
     ST_GeomFromText('POINT(40 50)', 4326));
 EXPR$0
 50.0
 !ok
 
 # Geometry example - units in planar degrees 4326 is WGS 84 long lat 
unit=degrees
-SELECT ST_Distance(
-    ST_GeomFromText('POINT(-72.1235 42.3521)',4326),
-    ST_GeomFromText('LINESTRING(-72.1260 42.45, -72.123 42.1546)', 4326));
+SELECT round(ST_Distance(
+    ST_GeomFromText('POINT(-72.1235 42.3521)', 4326),
+    ST_GeomFromText('LINESTRING(-72.1260 42.45, -72.123 42.1546)', 4326)), 10);
 EXPR$0
-0.0015056772638282166
+0.0015056773
 !ok
 
 # Geometry example - units in meters (SRID: 26986 Massachusetts state plane 
meters) (most accurate for Massachusetts)
-SELECT ST_Distance(
+SELECT round(ST_Distance(
     ST_Transform(ST_GeomFromText('POINT(-72.1235 42.3521)',4326),26986),
-    ST_Transform(ST_GeomFromText('LINESTRING(-72.1260 42.45, -72.123 
42.1546)', 4326),26986));
+    ST_Transform(ST_GeomFromText('LINESTRING(-72.1260 42.45, -72.123 
42.1546)', 4326),26986)), 10);
 EXPR$0
-0.0015056772638282166
+123.7979378784
 !ok
 
 # Geometry example - units in meters (SRID: 2163 US National Atlas Equal area) 
(least accurate)
-SELECT ST_Distance(
-    ST_Transform(ST_GeomFromText('POINT(-72.1235 42.3521)',4326),2163),
-    ST_Transform(ST_GeomFromText('LINESTRING(-72.1260 42.45, -72.123 
42.1546)', 4326),2163));
+SELECT round(ST_Distance(
+    ST_Transform(ST_GeomFromText('POINT(-72.1235 42.3521)', 4326), 2163),
+    ST_Transform(ST_GeomFromText('LINESTRING(-72.1260 42.45, -72.123 
42.1546)', 4326), 2163)), 10);
 
 EXPR$0
-0.0015056772638282166
+126.6642560574
 !ok
 
 # Disabled: PostgreSQL does geography, Calcite does not
@@ -487,22 +485,22 @@ FROM (SELECT
 
 SELECT ST_AsText(ST_Envelope(ST_GeomFromText('POINT(1 3)')));
 EXPR$0
-POLYGON ((1 3, 1 3, 1 3, 1 3, 1 3))
+POINT (1 3)
 !ok
 
 SELECT ST_AsText(ST_Envelope(ST_GeomFromText('LINESTRING(0 0, 1 3)')));
 EXPR$0
-POLYGON ((0 0, 1 0, 1 3, 0 3, 0 0))
+POLYGON ((0 0, 0 3, 1 3, 1 0, 0 0))
 !ok
 
 SELECT ST_AsText(ST_Envelope(ST_GeomFromText('POLYGON((0 0, 0 1, 1.0000001 1, 
1.0000001 0, 0 0))')));
 EXPR$0
-POLYGON ((0 0, 1.0000001 0, 1.0000001 1, 0 1, 0 0))
+POLYGON ((0 0, 0 1, 1.0000001 1, 1.0000001 0, 0 0))
 !ok
 
 SELECT ST_AsText(ST_Envelope(ST_GeomFromText('POLYGON((0 0, 0 1, 1.0000000001 
1, 1.0000000001 0, 0 0))')));
 EXPR$0
-POLYGON ((0 0, 1.0000000001 0, 1.0000000001 1, 0 1, 0 0))
+POLYGON ((0 0, 0 1, 1.0000000001 1, 1.0000000001 0, 0 0))
 !ok
 
 # ST_Explode(query [, fieldName]) Explodes the geometry-collections in the 
*fieldName* column of a query into multiple geometries
@@ -534,7 +532,7 @@ SELECT id, ST_GeometryType(g), ST_GeometryTypeCode(g) FROM 
(VALUES
                                        ((0 0, 2 2, 5 3, 0 0)))'))) AS t(id, g);
 ID, EXPR$1, EXPR$2
 ls, LINESTRING, 2
-mp, POLYGON, 3
+mp, MULTIPOLYGON, 6
 np, null, null
 p , POINT, 1
 !ok
@@ -809,34 +807,34 @@ false
 
 # ST_Buffer(geom, bufferSize [, quadSegs | style ]) Computes a buffer around 
*geom*
 
-SELECT ST_Buffer(
+SELECT roundGeom(ST_AsWKT(ST_Buffer(
  ST_GeomFromText('POINT(100 90)'),
- 50);
+ 50)), 10);
 EXPR$0
-{"rings":[[[150,90],[149.89294616193018,86.72984353849284],[149.5722430686905,83.47369038899743],[149.0392640201615,80.24548389919359],[148.2962913144534,77.05904774487396],[147.34650647475527,73.9...
 (3574 characters)
+POLYGON ((150 90, 149.0392640202 80.2454838992, 146.1939766256 70.8658283818, 
141.5734806152 62.2214883491, 135.3553390594 54.6446609407, 127.7785116510 
48.4265193849, 119.1341716183 43.8060233745, 109.7545161009 40.9607359799, 100 
40, 90.2454838992 40.9607359799, 80.8658283818 43.8060233745, 72.2214883491 
48.4265193849, 64.6446609407 54.6446609407, 58.4265193849 62.2214883491, 
53.8060233745 70.8658283818, 50.9607359799 80.2454838992, 50 90, 50.9607359799 
99.7545161009, 53.8060233745 109 [...]
 !ok
 
-SELECT ST_Buffer(
+SELECT roundGeom(ST_AsWKT(ST_Buffer(
  ST_GeomFromText('LINESTRING(10 10,30 10)'),
- 5);
+ 5)), 10);
 EXPR$0
-{"rings":[[[10,5],[9.672984353849284,5.010705383806982],[9.347369038899743,5.042775693130948],[9.02454838991936,5.096073597983848],[8.705904774487397,5.1703708685546585],[8.39280267348419,5.2653493...
 (3532 characters)
+POLYGON ((30 15, 30.9754516101 14.9039264021, 31.9134171619 14.6193976626, 
32.7778511651 14.1573480616, 33.5355339060 13.5355339060, 34.1573480616 
12.7778511651, 34.6193976626 11.9134171619, 34.9039264021 10.9754516101, 35 10, 
34.9039264021 9.0245483900, 34.6193976626 8.0865828382, 34.1573480616 
7.2221488350, 33.5355339060 6.4644660941, 32.7778511651 5.8426519385, 
31.9134171619 5.3806023375, 30.9754516101 5.0960735980, 30 5, 10 5, 
9.0245483900 5.0960735980, 8.0865828382 5.3806023375, 7.2 [...]
 !ok
 
-SELECT ST_Buffer(
+SELECT roundGeom(ST_AsWKT(ST_Buffer(
   ST_GeomFromText('POLYGON((-71.1776585052917 
42.3902909739571,-71.1776820268866 42.3903701743239,
     -71.1776063012595 42.3903825660754,-71.1775826583081 
42.3903033653531,-71.1776585052917 42.3902909739571))'),
- 50);
+ 50)), 10);
 EXPR$0
-{"rings":[[[-21.17763234259735,42.39033677001625],[-21.284686180667173,39.12018030850909],[-21.60538927390683,35.864027159013666],[-22.138368322435824,32.635820669209835],[-22.881341028143936,29.44...
 (3872 characters)
+POLYGON ((-63.1158577185 -6.9555028496, -73.3944111820 -7.5605449307, 
-83.5789612644 -6.0473771149, -93.2376230056 -2.4801666374, -101.9608122109 
2.9898157492, -109.3786142359 10.1306105467, -115.1764705475 18.6394059614, 
-119.1085178604 28.1553789096, -119.1085413820 28.1554581099, -120.9273691116 
37.3935076607, -120.9820790302 46.8087453442, -119.2707311309 56.0673079853, 
-115.8540096006 64.8408880867, -110.8530709662 72.8183755726, -104.4452478994 
79.7168897137, -96.8577610211 85.2918 [...]
 !ok
 
 # Negative buffer size makes the polgyon smaller
 SELECT ST_Buffer(
-  ST_GeomFromText('POLYGON((10 10,10 20,20 20,20 10))'),
+  ST_GeomFromText('POLYGON((10 10,10 20,20 20,20 10, 10 10))'),
  -1);
 EXPR$0
-{"rings":[[[11,11],[11,19],[19,19],[19,11],[11,11]]]}
+POLYGON ((11 11, 11 19, 19 19, 19 11, 11 11))
 !ok
 
 !if (fixed.calcite2539) {
@@ -875,14 +873,14 @@ SELECT ST_AsText(ST_Union(ST_GeomFromText('POINT(1 2)'),
     ST_GeomFromText('POINT(-2 3)')));
 
 EXPR$0
-MULTIPOINT ((1 2), (-2 3))
+MULTIPOINT ((-2 3), (1 2))
 !ok
 
 # NOTE: PostGIS returned a point not a multipoint: POINT(1 2). ESRI bug?
 SELECT ST_AsText(ST_Union(ST_GeomFromText('POINT(1 2)'),
     ST_GeomFromText('POINT(1 2)')));
 EXPR$0
-MULTIPOINT ((1 2))
+POINT (1 2)
 !ok
 
 # ST_Union(geomCollection) Computes the union of two or more geometries
@@ -1064,25 +1062,22 @@ MULTILINESTRING((3 4,4 5),(1 2,3 4))
 #### Geometry projection functions
 
 # ST_SetSRID(geom, srid) Returns a copy of *geom* with a new SRID
-
-SELECT ST_SetSRID(ST_MakePoint(-123.365556, 48.428611),4326) As wgs84long_lat;
+SELECT ST_AsEWKT(ST_SetSRID(ST_MakePoint(-123.365556, 48.428611),4326)) As 
wgs84long_lat;
 WGS84LONG_LAT
-{"x":-123.365556,"y":48.428611}
+srid:4326;POINT (-123.365556 48.428611)
 !ok
 
 # Mark a point as WGS 84 long lat and then transform to web mercator 
(Spherical Mercator)
-SELECT ST_Transform(ST_SetSRID(ST_MakePoint(-123.365556, 
48.428611),4326),3785) As sphere_merc;
+SELECT ST_AsEWKT(ST_Transform(ST_SetSRID(ST_MakePoint(-123.365556, 
48.428611),4326),3785)) As sphere_merc;
 SPHERE_MERC
-{"x":-123.365556,"y":48.428611,"spatialReference":{"wkid":102113,"latestWkid":3785}}
+srid:9804;POINT (-13732990.875349075 6178458.964254234)
 !ok
 
 # ST_Transform(geom, srid) Transforms *geom* from one coordinate reference 
system (CRS) to the CRS specified by *srid*
-
-SELECT ST_AsText(ST_Transform(ST_GeomFromText('POLYGON((743238 2967416,743238 
2967450,
-  743265 2967450,743265.625 2967416,743238 2967416))',2249),4326)) As wgs_geom;
-
+SELECT roundGeom(ST_AsText(ST_Transform(ST_GeomFromText('POLYGON((743238 
2967416,743238 2967450,
+  743265 2967450,743265.625 2967416,743238 2967416))',2249),4326)), 10) As 
wgs_geom;
 WGS_GEOM
-MULTIPOLYGON (((743238 2967416, 743265.625 2967416, 743265 2967450, 743238 
2967450, 743238 2967416)))
+POLYGON ((-71.1776848523 42.3902896513, -71.1776843767 42.3903829479, 
-71.1775844306 42.3903826678, -71.1775825928 42.3902893648, -71.1776848523 
42.3902896513))
 !ok
 
 #### Trigonometry functions
@@ -1156,10 +1151,10 @@ FROM (
   SELECT ST_GeomFromText('LINESTRING Z(5 5 5, 10 10 10)') as the_geom ) as foo;
 
 EXPR$0
-MULTILINESTRING Z ((5 5 5, 10 10 10))
-MULTIPOLYGON (((-7 4.2, -7.1 4.3, -7.1 4.2, -7 4.2)))
-POINT Z (-2 3 1)
-POINT Z (5 5 5)
+LINESTRING Z(5 5 5, 10 10 10)
+POINT Z(-2 3 1)
+POINT Z(5 5 5)
+POLYGON ((-7 4.2, -7.1 4.2, -7.1 4.3, -7 4.2))
 !ok
 
 # 3d example not mixing dimensions
@@ -1175,10 +1170,10 @@ FROM (
   SELECT ST_GeomFromText('LINESTRING Z(5 5 5, 10 10 10)') as the_geom ) as foo;
 
 EXPR$0
-MULTILINESTRING Z ((5 5 5, 10 10 10))
-MULTIPOLYGON Z (((-7 4.2 2, -7.1 4.3 2, -7.1 4.2 3, -7 4.2 2)))
-POINT Z (-2 3 1)
-POINT Z (5 5 5)
+LINESTRING Z(5 5 5, 10 10 10)
+POINT Z(-2 3 1)
+POINT Z(5 5 5)
+POLYGON Z((-7 4.2 2, -7.1 4.2 3, -7.1 4.3 2, -7 4.2 2))
 !ok
 
 # Polygon-to-polygon joins
@@ -1186,26 +1181,26 @@ select *
 from GEO."states" as s
 order by "name";
 name, geom
-AZ, 
{"rings":[[[-114,37],[-109.05,37],[-109.05,31.33],[-111.07,31.33],[-114.75,32.5],[-114.75,35.1],[-114,37]]]}
-CA, 
{"rings":[[[-124.25,42],[-120,42],[-120,39],[-114.75,35.1],[-114.75,32.5],[-117.15,32.5],[-118.3,33.75],[-120.5,34.5],[-122.4,37.2],[-124.25,42]]]}
-CO, {"rings":[[[-109.05,41],[-102,41],[-102,37],[-109.05,37],[-109.05,41]]]}
-ID, 
{"rings":[[[-117,49],[-116.05,49],[-116.05,48],[-114.4,46.6],[-112.9,44.45],[-111.05,44.45],[-111.05,42],[-117.03,42],[-117.03,44.2],[-116.5,45.5],[-117,46],[-117,49]]]}
-MT, 
{"rings":[[[-116.05,49],[-104.05,49],[-104.05,45],[-111.05,45],[-111.05,44.45],[-112.9,44.45],[-114.4,46.6],[-116.05,48],[-116.05,49]]]}
-NM, 
{"rings":[[[-109.05,37],[-103,37],[-103,32],[-106.65,32],[-106.5,31.8],[-108.2,31.8],[-108.2,31.33],[-109.05,31.33],[-109.05,37]]]}
-NV, 
{"rings":[[[-120,42],[-114,42],[-114,37],[-114.75,35.1],[-120,39],[-120,42]]]}
-OR, 
{"rings":[[[-123.9,46.2],[-122.7,45.7],[-119,46],[-117,46],[-116.5,45.5],[-117.03,44.2],[-117.03,42],[-124.25,42],[-124.6,42.8],[-123.9,46.2]]]}
-UT, 
{"rings":[[[-114,42],[-111.05,42],[-111.05,41],[-109.05,41],[-109.05,37],[-114,37],[-114,42]]]}
-WA, 
{"rings":[[[-124.8,48.4],[-123.2,48.2],[-123.2,49],[-117,49],[-117,46],[-119,46],[-122.7,45.7],[-123.9,46.2],[-124.8,48.4]]]}
-WY, 
{"rings":[[[-111.05,45],[-104.05,45],[-104.05,41],[-111.05,41],[-111.05,45]]]}
+AZ, POLYGON ((-114 37, -109.05 37, -109.05 31.33, -111.07 31.33, -114.75 32.5, 
-114.75 35.1, -114 37))
+CA, POLYGON ((-124.25 42, -120 42, -120 39, -114.75 35.1, -114.75 32.5, 
-117.15 32.5, -118.3 33.75, -120.5 34.5, -122.4 37.2, -124.25 42))
+CO, POLYGON ((-109.05 41, -102 41, -102 37, -109.05 37, -109.05 41))
+ID, POLYGON ((-117 49, -116.05 49, -116.05 48, -114.4 46.6, -112.9 44.45, 
-111.05 44.45, -111.05 42, -117.03 42, -117.03 44.2, -116.5 45.5, -117 46, -117 
49))
+MT, POLYGON ((-116.05 49, -104.05 49, -104.05 45, -111.05 45, -111.05 44.45, 
-112.9 44.45, -114.4 46.6, -116.05 48, -116.05 49))
+NM, POLYGON ((-109.05 37, -103 37, -103 32, -106.65 32, -106.5 31.8, -108.2 
31.8, -108.2 31.33, -109.05 31.33, -109.05 37))
+NV, POLYGON ((-120 42, -114 42, -114 37, -114.75 35.1, -120 39, -120 42))
+OR, POLYGON ((-123.9 46.2, -122.7 45.7, -119 46, -117 46, -116.5 45.5, -117.03 
44.2, -117.03 42, -124.25 42, -124.6 42.8, -123.9 46.2))
+UT, POLYGON ((-114 42, -111.05 42, -111.05 41, -109.05 41, -109.05 37, -114 
37, -114 42))
+WA, POLYGON ((-124.8 48.4, -123.2 48.2, -123.2 49, -117 49, -117 46, -119 46, 
-122.7 45.7, -123.9 46.2, -124.8 48.4))
+WY, POLYGON ((-111.05 45, -104.05 45, -104.05 41, -111.05 41, -111.05 45))
 !ok
 
 select *
 from GEO."parks" as p
 order by "name";
 name, geom
-Death Valley NP, 
{"rings":[[[-118.2,37.3],[-117,37],[-116.3,35.7],[-117,35.7],[-117.2,36.2],[-117.8,36.4],[-118.2,37.3]]]}
-Yellowstone NP, 
{"rings":[[[-111.2,45.1],[-109.3,45.1],[-109.3,44.1],[-109,43.8],[-110,43],[-111.2,43.4],[-111.2,45.1]]]}
-Yosemite NP, 
{"rings":[[[-120.2,38],[-119.3,38.2],[-119,37.7],[-119.9,37.6],[-120.2,38]]]}
+Death Valley NP, POLYGON ((-118.2 37.3, -117 37, -116.3 35.7, -117 35.7, 
-117.2 36.2, -117.8 36.4, -118.2 37.3))
+Yellowstone NP, POLYGON ((-111.2 45.1, -109.3 45.1, -109.3 44.1, -109 43.8, 
-110 43, -111.2 43.4, -111.2 45.1))
+Yosemite NP, POLYGON ((-120.2 38, -119.3 38.2, -119 37.7, -119.9 37.6, -120.2 
38))
 !ok
 
 # Parks that may intersect states
diff --git 
a/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeSchemaFactory.java
 
b/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeSchemaFactory.java
index 1c3ad623d4..5c1f5a679b 100644
--- 
a/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeSchemaFactory.java
+++ 
b/geode/src/main/java/org/apache/calcite/adapter/geode/rel/GeodeSchemaFactory.java
@@ -17,7 +17,7 @@
 package org.apache.calcite.adapter.geode.rel;
 
 import org.apache.calcite.model.ModelHandler;
-import org.apache.calcite.runtime.GeoFunctions;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.SchemaFactory;
 import org.apache.calcite.schema.SchemaPlus;
@@ -61,7 +61,7 @@ public class GeodeSchemaFactory implements SchemaFactory {
 
     if (allowSpatialFunctions) {
       ModelHandler.addFunctions(parentSchema, null, ImmutableList.of(),
-          GeoFunctions.class.getName(), "*", true);
+          SpatialTypeFunctions.class.getName(), "*", true);
     }
 
     return new GeodeSchema(
diff --git a/gradle.properties b/gradle.properties
index 232017709f..66f4ccb1d9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -95,7 +95,9 @@ dropwizard-metrics.version=4.0.5
 # do not upgrade this, new versions are Category X license.
 elasticsearch.version=7.10.2
 embedded-redis.version=0.6
-esri-geometry-api.version=2.2.0
+jts-core.version=1.19.0
+jts-io-common.version=1.19.0
+proj4j.version=1.1.5
 foodmart-data-hsqldb.version=0.5
 foodmart-data-json.version=0.4
 foodmart-queries.version=0.4.1
diff --git a/src/main/config/checkstyle/suppressions.xml 
b/src/main/config/checkstyle/suppressions.xml
index c51a9c370f..73a1cdaba0 100644
--- a/src/main/config/checkstyle/suppressions.xml
+++ b/src/main/config/checkstyle/suppressions.xml
@@ -45,7 +45,7 @@ limitations under the License.
   <suppress checks="JavadocPackage" 
files="src[/\\]main[/\\]java[/\\]org[/\\]apache[/\\]calcite[/\\]test[/\\]schemata[/\\]"/>
 
   <!-- Don't complain about method names in a class full of UDFs -->
-  <suppress checks="MethodName" files="GeoFunctions.java"/>
+  <suppress checks="MethodName" files="SpatialTypeFunctions.java"/>
 
   <!-- Suppress JavadocPackage in the test packages,
        and in the 'org.apache.calcite.server' package, which is split between
diff --git a/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java 
b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
index 473ae6c30a..2aa10f6b2e 100644
--- a/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
+++ b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
@@ -42,8 +42,8 @@ import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeImpl;
 import org.apache.calcite.rel.type.RelProtoDataType;
 import org.apache.calcite.runtime.CalciteException;
-import org.apache.calcite.runtime.GeoFunctions;
 import org.apache.calcite.runtime.Hook;
+import org.apache.calcite.runtime.SpatialTypeFunctions;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.SchemaVersion;
@@ -60,7 +60,7 @@ import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlDialect;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.sql.SqlNode;
-import org.apache.calcite.sql.fun.SqlGeoFunctions;
+import org.apache.calcite.sql.fun.SqlSpatialTypeFunctions;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.sql.validate.SqlValidatorException;
@@ -820,9 +820,11 @@ public class CalciteAssert {
       return rootSchema.add("foodmart2", new CloneSchema(foodmart));
     case GEO:
       ModelHandler.addFunctions(rootSchema, null, emptyPath,
-          GeoFunctions.class.getName(), "*", true);
+          SpatialTypeFunctions.class.getName(), "*", true);
       ModelHandler.addFunctions(rootSchema, null, emptyPath,
-          SqlGeoFunctions.class.getName(), "*", true);
+          SqlSpatialTypeFunctions.class.getName(), "*", true);
+      ModelHandler.addFunctions(rootSchema, "roundGeom", emptyPath,
+          TestUtil.class.getName(), "roundGeom", true);
       final SchemaPlus s =
           rootSchema.add(schema.schemaName, new AbstractSchema());
       ModelHandler.addFunctions(s, "countries", emptyPath,
diff --git a/testkit/src/main/java/org/apache/calcite/util/TestUtil.java 
b/testkit/src/main/java/org/apache/calcite/util/TestUtil.java
index 15c332c54e..7e7870df7e 100644
--- a/testkit/src/main/java/org/apache/calcite/util/TestUtil.java
+++ b/testkit/src/main/java/org/apache/calcite/util/TestUtil.java
@@ -248,6 +248,69 @@ public abstract class TestUtil {
     return s;
   }
 
+  /** Rounds all decimal fractions inside a string to a given number of decimal
+   * places.
+   *
+   * <p>For example,
+   * {@code round("POINT(-1.23456, 9.87654)", 3)}
+   * returns "POINT(-1.235, 9.877)". */
+  public static String roundGeom(String s, int precision) {
+    final StringBuilder b = new StringBuilder();
+    boolean carried = false;
+    int end = -1;
+    for (int i = 0; i < s.length(); i++) {
+      char c = s.charAt(i);
+      switch (c) {
+      case '.':
+        // Entering the fractional part of a number
+        end = i + precision + 1;
+        carried = false;
+        break;
+      case '0': case '1': case '2': case '3': case '4':
+      case '5': case '6': case '7': case '8': case '9':
+        if (end < 0) {
+          break; // We've not seen a '.'
+        }
+        if (i < end) {
+          break; // Have seen a '.' but not enough digits yet
+        }
+        if (c > '5' || i > end && c > '0') {
+          if (!carried) {
+            carry(b);
+            carried = true;
+          }
+        }
+        continue;
+      default:
+        end = -1; // no longer in a number
+      }
+      b.append(c);
+    }
+    return b.toString();
+  }
+
+  /** Increments the last digit of a decimal number in a StringBuilder, and if
+   * that digit was a '9', carries on going. */
+  private static void carry(StringBuilder b) {
+    for (int i = b.length() - 1; i >= 0; i--) {
+      char c = b.charAt(i);
+      switch (c) {
+      case '.':
+        continue; // continue to the left of decimal point
+      case '0': case '1': case '2': case '3': case '4':
+      case '5': case '6': case '7': case '8':
+        b.setCharAt(i, (char) (c + 1)); // carry, and we're done
+        return;
+      case '9':
+        b.setCharAt(i, '0'); // '9' becomes '0', and continue carrying
+        continue;
+      default:
+        b.insert(i + 1, '1');
+        return;
+      }
+    }
+  }
+
   /**
    * Returns the Java major version: 7 for JDK 1.7, 8 for JDK 8, 10 for
    * JDK 10, etc. depending on current system property {@code java.version}.
diff --git a/testkit/src/test/java/org/apache/calcite/util/TestUtilTest.java 
b/testkit/src/test/java/org/apache/calcite/util/TestUtilTest.java
index 79056be35d..e5bf1477fa 100644
--- a/testkit/src/test/java/org/apache/calcite/util/TestUtilTest.java
+++ b/testkit/src/test/java/org/apache/calcite/util/TestUtilTest.java
@@ -28,6 +28,8 @@ import java.util.List;
 import java.util.SortedSet;
 import java.util.stream.Collectors;
 
+import static org.apache.calcite.util.TestUtil.roundGeom;
+
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.Is.is;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -38,6 +40,24 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  */
 class TestUtilTest {
 
+  /** Tests {@link TestUtil#roundGeom}. */
+  @Test void testRoundGeom() {
+    assertThat(roundGeom("1", 3), is("1"));
+    assertThat(roundGeom("1.28", 3), is("1.28"));
+    assertThat(roundGeom("1.278", 3), is("1.278"));
+    assertThat(roundGeom("1.2784", 3), is("1.278")); // < 5 rounds down
+    assertThat(roundGeom("1.2785", 3), is("1.278")); // = 5 rounds down
+    assertThat(roundGeom("1.27850001", 3), is("1.279")); // > 5 rounds up
+    assertThat(roundGeom("1.27950001", 3), is("1.280"));
+    assertThat(roundGeom("1.29950001", 3), is("1.300"));
+    assertThat(roundGeom("19.99950001", 3), is("20.000"));
+    assertThat(roundGeom("2 9.99950001", 3), is("2 10.000"));
+    assertThat(roundGeom("23 9.99950001", 3), is("23 10.000"));
+    assertThat(roundGeom("234 9.99950001", 3), is("234 10.000"));
+    assertThat(roundGeom("POINT(-1.23456, 9.87654)", 3),
+        is("POINT(-1.235, 9.877)"));
+  }
+
   @Test void javaMajorVersionExceeds6() {
     // shouldn't throw any exceptions (for current JDK)
     int majorVersion = TestUtil.getJavaMajorVersion();

Reply via email to