code: package it.unibz.inf.ontop.model.term.functionsymbol.impl.geof;
  
  import it.unibz.inf.ontop.model.term.*;
  import it.unibz.inf.ontop.model.term.functionsymbol.db.DBConcatFunctionSymbol;
  import it.unibz.inf.ontop.model.term.functionsymbol.db.impl.DefaultSimpleDBCastFunctionSymbol;
  import it.unibz.inf.ontop.utils.ImmutableCollectors;
  import org.apache.commons.rdf.api.IRI;
  import org.apache.commons.rdf.api.RDF;
  import org.apache.commons.rdf.simple.SimpleRDF;
  import org.locationtech.proj4j.CRSFactory;
  import org.locationtech.proj4j.CoordinateReferenceSystem;
  import org.locationtech.proj4j.units.Unit;
  
  import java.util.Optional;
  
  public class GeoUtils {
  
      static final double EARTH_MEAN_RADIUS_METER = 6370986;
  
      private static final RDF rdfFactory = new SimpleRDF();
  
      /**
       * The default GeoSPARQL SRID - CRS84
       */
      public static final IRI defaultSRID = rdfFactory.createIRI("http://www.opengis.net/def/crs/OGC/1.3/CRS84");
  
  
      /**
       * Method returns the WKTLiteralValue constructed using the SRID and geometry
       * SRID: If no SRID defined, default CRS84 SRID is assigned. If the input is provided at query time
       * the SRID is extracted from a constant. Otherwise minimal parsing used to extract from template.
       * Geometry: If constant input at query tie {@link #tryExtractGeometryFromConstant(ImmutableTerm, TermFactory, IRI)}
       * Geometry: If DB attribute {@link #tryExtractGeometryFromTemplate(TermFactory, ImmutableTerm)}
       *
       * @param termFactory
       * @param wktLiteralTerm
       * @return WKTLiteralValue which is composed of the SRID and geometry
       */
      static WKTLiteralValue extractWKTLiteralValue(TermFactory termFactory, ImmutableTerm wktLiteralTerm) {
          // Get the respective SRID
          IRI srid = tryExtractSRIDFromDbConstant(Optional.of(wktLiteralTerm))
                  .orElseGet(
                          // template
                          () -> tryExtractSRIDFromTemplate(wktLiteralTerm)
                                  // otherwise, returns the default SRID
                                  .orElse(defaultSRID)
                  );
  
          // Get the respective geometry
          ImmutableTerm geometry = tryExtractGeometryFromConstant(wktLiteralTerm, termFactory, srid)
                  .orElseGet(
                          // If template then
                          () -> tryExtractGeometryFromTemplate(termFactory, wktLiteralTerm)
                                  // If nested geosparql expression e.g. ST_WITHIN(geom0, ST_BUFFER(geom1))
                                  .orElseGet(() -> tryExtractNestedGeoSPARQLExpression(wktLiteralTerm)
                                          .orElse(wktLiteralTerm))
                  );
  
          return new WKTLiteralValue(srid, geometry);
      }
  
      /**
       * Extracts the SRID from a geometry input at SPARQL query time by a user
       * e.g. from input "<http://www.opengis.net/def/crs/EPSG/0/3044> POINT(6.6441878 49.7596208)"
       * output is 3044
       *
       * @param immutableTerm
       * @return SRID
       */
      @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
      static Optional<IRI> tryExtractSRIDFromDbConstant(Optional<ImmutableTerm> immutableTerm) {
          return immutableTerm
                  // the first argument has to be a constant
                  .filter(t -> t instanceof DBConstant).map(t -> (DBConstant) t)
                  .map(Constant::getValue)
                  // the SRID is enclosed by "<" and ">
                  .filter(v -> v.startsWith("<") && v.indexOf(">") > 0)
                  // extract the SRID out of the string
                  .map(v -> v.substring(1, v.indexOf(">")))
                  .map(rdfFactory::createIRI);
      }
  
      /**
       * Extract SRID from any geospatial template
       *
       * @param wktLiteralTerm
       * @return SRID
       */
      private static Optional<IRI> tryExtractSRIDFromTemplate(ImmutableTerm wktLiteralTerm) {
          Optional<ImmutableTerm> firstTermFromTemplate = Optional.of(wktLiteralTerm)
                  // template is a NonGroundFunctionalTerm
                  .filter(t -> t instanceof NonGroundFunctionalTerm).map(t -> (NonGroundFunctionalTerm) t)
                  // template uses DBConcatFunctionSymbol as the functional symbol
                  .filter(t -> t.getFunctionSymbol() instanceof DBConcatFunctionSymbol)
                  // Always return the first argument
                  .map(t -> t.getTerm(0));
          // check if the first argument is the string starting with the IRI of the SRID
          return tryExtractSRIDFromDbConstant(firstTermFromTemplate);
      }
  
      /**
       * Method extracts geometry from constant input at query time by user.
       * The SRID of the input is set to match that of the SRID IRI template or default SRID if none provided.
       * e.g. for geof:distance("<http://www.opengis.net/def/crs/EPSG/0/3044> POINT(6.6441878 49.7596208)", geom1)
       * the SRID of POINT(6.6441878 49.7596208) has to be set to 3044
       * Method used in DB is ST_SETSRID and must be specified with the geometry in this instance
       * If no SRID IRI is provided, the default SRID of 4326 is set (since some DBs would otherwise set this to 0)
       *
       * @param immutableTerm
       * @param termFactory
       * @param srid
       * @return geometry with respective srid setting (either derived from SRID IRI or defaultSRID)
       */
      static Optional<ImmutableTerm> tryExtractGeometryFromConstant(ImmutableTerm immutableTerm, TermFactory termFactory, IRI srid) {
  
          // Find which SRID to set, constants have SRID=0 in PostG
          final String EPSG_PREFIX = "EPSG:";
          String sridProj4j = toProj4jName(srid.getIRIString());
          DBConstant newEPSG = (sridProj4j.equals("CRS:84"))
                  ? termFactory.getDBStringConstant("4326")
                  : termFactory.getDBStringConstant(sridProj4j.substring(EPSG_PREFIX.length()));
  
          return Optional.of(immutableTerm)
                  // template is NOT a NonGroundFunctionalTerm, but a string user input
                  .filter(t -> t instanceof DBConstant).map(t -> (DBConstant) t)
                  .map(Constant::getValue)
                  // extract the geometry out of the string
                  .map(v -> termFactory.getDBStringConstant(v.substring(v.indexOf(">") + 1).trim()))
                  // Use GeomFromText with one argument to convert text into geometry
                  .map(termFactory::getDBSTGeomFromText)
                  // Set SRID
                  .map(v -> termFactory.getDBSTSetSRID(v, newEPSG))
                  // Convert to text
                  .map(termFactory::getDBAsText);
      }
  
      /**
       * Method extracts geometry from non-constant database data.
       * Drop any SRID template provided by user at query time by removing the first term.
       *
       * @param termFactory
       * @param wktLiteralTerm
       * @return geometry without SRID
       */
      private static Optional<ImmutableTerm> tryExtractGeometryFromTemplate(TermFactory termFactory, ImmutableTerm wktLiteralTerm) {
          return Optional.of(wktLiteralTerm)
                  // template is a NonGroundFunctionalTerm
                  .filter(t -> t instanceof NonGroundFunctionalTerm).map(t -> (NonGroundFunctionalTerm) t)
                  // template uses DBConcatFunctionSymbol as the functional symbol
                  .filter(t -> t.getFunctionSymbol() instanceof DBConcatFunctionSymbol)
                  // check if the first argument is the string starting with the IRI of the SRID
                  .map(t -> {
                      // if the term does not start with the SRID, the whole term is geometry
                      if (!templateStartsWithSRID(t.getTerm(0))) {
                          return wktLiteralTerm;
                      }
                      // SRID + geometry
                      else {
                          // return geometry which is index = 1 (special case since DBNullRejConcat needs 2+ args)
                          if (t.getTerms().size() == 2)
                              return t.getTerm(1);
                          else
                              // Otherwise drop the SRID template, and return everything else
                              return termFactory.getNullRejectingDBConcatFunctionalTerm(
                                      t.getTerms()
                                              .stream()
                                              // If argument with SRID, drop SRID, otherwise keep terms
                                              .map(subterm -> subterm.toString().contains("<")
                                                      ? extractConstantWKTLiteralValue(termFactory, subterm).get()
                                                      : subterm)
                                              .collect(ImmutableCollectors.toList()));
                      }
                  });
      }
  
      /**
       * Method extracts geosparql expression when nested inside another geosparql expression.
       * Non-topological expressions such as ST_BUFFER, ST_INTERSECTION etc. get wrapped with ST_ASTEXT to yield a wkt.
       * If that expression is an argument of another geosparql function, SRID information is lost since ST_ASTEXT yields SRID=0
       * Method extracts geosparql expression from BOOLEANtoTEXT(ST_ASTEXT(geosparql_expression))
       *
       * @param wktLiteralTerm
       * @return geosparql expression which preserves the SRID
       */
      private static Optional<ImmutableTerm> tryExtractNestedGeoSPARQLExpression(ImmutableTerm wktLiteralTerm) {
  
          return Optional.of(wktLiteralTerm)
                  // GeoSPARQL expression can be either NonGroundFunctionalTerm or GroundFunctionalTerm
                  .filter(t -> t instanceof ImmutableFunctionalTerm).map(t -> (ImmutableFunctionalTerm) t)
                  // template uses DefaultDBCastFunctionSymbol as the functional symbol i.e. BOOLEANtoTEXT
                  .filter(t -> t.getFunctionSymbol() instanceof DefaultSimpleDBCastFunctionSymbol)
                  // check only one argument present i.e. ST_ASTEXT
                  .filter(t -> t.getTerms().size()==1)
                  .map(t -> t.getTerm(0))
                  // extract the geosparql expression inside ST_ASTEXT which preserves the SRID
                  .filter(t -> t instanceof ImmutableFunctionalTerm).map(t -> (ImmutableFunctionalTerm) t)
                  .filter(t -> t.getFunctionSymbol().toString().startsWith("ST_ASTEXT"))
                  .map(t -> t.getTerm(0));
      }
  
      /**
       * Check whether the template starts with SRID e.g. {@code <http://www.opengis.net/def/crs/OGC/1.3/CRS84>}
       *
       * @param immutableTerm
       * @return boolean yes=template starts with SRID
       */
      private static boolean templateStartsWithSRID(ImmutableTerm immutableTerm) {
          return Optional.of(immutableTerm)
                  .filter(t -> t instanceof DBConstant).map(t -> (DBConstant) t)
                  .map(Constant::getValue)
                  .filter(v -> v.startsWith("<") && v.indexOf(">") > 0)
                  .isPresent();
      }
  
      /**
       * Method extracts geometry without any SRID information.
       * This includes any template {GEOM} or {Lng}{Lat} which has a preceding SRID IRI
       *
       * @param termFactory
       * @param immutableTerm
       * @return geometry string without SRID
       */
      static Optional<ImmutableTerm> extractConstantWKTLiteralValue(TermFactory termFactory, ImmutableTerm immutableTerm) {
          // Get the respective geometry
          return Optional.of(immutableTerm)
                  // template is NOT a NonGroundFunctionalTerm, but a string user input
                  .filter(t -> t instanceof DBConstant).map(t -> (DBConstant) t)
                  .map(Constant::getValue)
                  // extract the geometry out of the string
                  .map(v -> termFactory.getDBStringConstant(v.substring(v.indexOf(">") + 1).trim()));
      }
  
      /**
       * Generates the abbreviated SRID from the full SRID IRI
       * Distinguishes between the default SRID CRS84 and other SRIDs with prefix EPSG
       *
       * @param sridIRIString
       * @return Abbreviated SRID string
       */
      static String toProj4jName(String sridIRIString) {
          final String CRS_PREFIX = "http://www.opengis.net/def/crs/OGC/1.3/CRS";
          final String EPSG_PREFIX = "http://www.opengis.net/def/crs/EPSG/0/";
  
          if (sridIRIString.startsWith(CRS_PREFIX)) {
              return "CRS:" + sridIRIString.substring(CRS_PREFIX.length());
          } else if (sridIRIString.startsWith(EPSG_PREFIX)) {
              return "EPSG:" + sridIRIString.substring(EPSG_PREFIX.length());
          }
  
          // TODO: other cases
          throw new IllegalArgumentException("Unknown SRID IRI: " + sridIRIString);
      }
  
      /**
       * Based on the abbreviated SRID denomination deduces the unit of the distance measure
       * The units generally include metre, degree and radian
       *
       * @param sridIRIString
       * @return unit of distance
       */
      public static DistanceUnit getUnitFromSRID(String sridIRIString) {
          String csName = toProj4jName(sridIRIString);
          if (csName.equals("CRS:84")) {
              return DistanceUnit.DEGREE;
          } else if (csName.startsWith("CRS:")) {
              throw new IllegalArgumentException("Unknown SRID IRI: " + sridIRIString);
          } else {
              CRSFactory csFactory = new CRSFactory();
              CoordinateReferenceSystem crs = csFactory.createFromName(csName);
              Unit proj4JUnit = crs.getProjection().getUnits();
              return DistanceUnit.findByName(proj4JUnit.name);
          }
      }
  }
  