1 /***********************************************************************
2  * Copyright (c) 2013-2024 Commonwealth Computer Research, Inc.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Apache License, Version 2.0
5  * which accompanies this distribution and is available at
6  * http://www.opensource.org/licenses/apache2.0.php.
7  ***********************************************************************/
8 
9 package org.locationtech.geomesa.index.utils
10 
11 import org.geotools.api.data.Query
12 import org.geotools.api.feature.simple.{SimpleFeature, SimpleFeatureType}
13 import org.geotools.api.referencing.crs.CoordinateReferenceSystem
14 import org.geotools.feature.FeatureTypes
15 import org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer
16 import org.geotools.referencing.CRS
17 import org.locationtech.geomesa.features.ScalaSimpleFeature
18 import org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes
19 import org.locationtech.jts.geom.Geometry
20 
21 /**
22   * Reproject the geometries in a simple feature to a different CRS
23   */
24 trait Reprojection {
25   def apply(feature: SimpleFeature): SimpleFeature
26 }
27 
28 object Reprojection {
29 
30   import org.locationtech.geomesa.index.conf.QueryHints.RichHints
31 
32   /**
33     * Create a reprojection function
34     *
35     * @param returnSft simple feature type being returned
36     * @param crs crs information from a query
37     * @return
38     */
39   def apply(returnSft: SimpleFeatureType, crs: QueryReferenceSystems): Reprojection = {
40     if (crs.target != crs.user) {
41       val transformer = new GeometryCoordinateSequenceTransformer
42       transformer.setMathTransform(CRS.findMathTransform(crs.user, crs.target, true))
43       val transformed = FeatureTypes.transform(returnSft, crs.target) // note: drops user data
44       new TransformReprojection(SimpleFeatureTypes.immutable(transformed, returnSft.getUserData), transformer)
45     } else if (crs.user != crs.native) {
46       val transformed = FeatureTypes.transform(returnSft, crs.user) // note: drops user data
47       new UserReprojection(SimpleFeatureTypes.immutable(transformed, returnSft.getUserData))
48     } else {
49       throw new IllegalArgumentException(s"Trying to reproject to the same CRS: $crs")
50     }
51   }
52 
53   /**
54     * Holds query projection info
55     *
56     * @param native native crs of the data
57     * @param user user crs for the query (data will be treated as this crs but without any transform)
58     * @param target target crs for the query (data will be transformed to this crs)
59     */
60   case class QueryReferenceSystems(
61       native: CoordinateReferenceSystem,
62       user: CoordinateReferenceSystem,
63       target: CoordinateReferenceSystem)
64 
65   object QueryReferenceSystems {
66     def apply(query: Query): Option[QueryReferenceSystems] = {
67       Option(query.getHints.getReturnSft.getGeometryDescriptor).flatMap { descriptor =>
68         val native = descriptor.getCoordinateReferenceSystem
69         val source = Option(query.getCoordinateSystem).getOrElse(native)
70         val target = Option(query.getCoordinateSystemReproject).getOrElse(native)
71         if (target == source && source == native) { None } else {
72           Some(QueryReferenceSystems(native, source, target))
73         }
74       }
75     }
76   }
77 
78   /**
79     * Applies a geometric transform to any geometry attributes
80     *
81     * @param sft simple feature type being projected to
82     * @param transformer transformer
83     */
84   private class TransformReprojection(sft: SimpleFeatureType, transformer: GeometryCoordinateSequenceTransformer)
85       extends Reprojection {
86     override def apply(feature: SimpleFeature): SimpleFeature = {
87       val values = Array.tabulate(sft.getAttributeCount) { i =>
88         feature.getAttribute(i) match {
89           case g: Geometry => transformer.transform(g)
90           case a => a
91         }
92       }
93       new ScalaSimpleFeature(sft, feature.getID, values, feature.getUserData)
94     }
95   }
96 
97   /**
98     * Changes the defined crs but does not do any actual geometric transforms
99     *
100     * @param sft simple feature type being projected to
101     */
102   private class UserReprojection(sft: SimpleFeatureType) extends Reprojection {
103     override def apply(feature: SimpleFeature): SimpleFeature = ScalaSimpleFeature.copy(sft, feature)
104   }
105 }
Line Stmt Id Pos Tree Symbol Tests Code
40 44000 1529 - 1537 Select org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems.user crs.user
40 44001 1515 - 1537 Apply java.lang.Object.!= crs.target.!=(crs.user)
40 44013 1539 - 1904 Block <nosymbol> { val transformer: org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer = new org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer(); transformer.setMathTransform(org.geotools.referencing.CRS.findMathTransform(crs.user, crs.target, true)); val transformed: org.geotools.api.feature.simple.SimpleFeatureType = org.geotools.feature.FeatureTypes.transform(returnSft, crs.target); new Reprojection.this.TransformReprojection(org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable(transformed, returnSft.getUserData()), transformer) }
41 44002 1565 - 1606 Apply org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer.<init> new org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer()
42 44003 1664 - 1672 Select org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems.user crs.user
42 44004 1674 - 1684 Select org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems.target crs.target
42 44005 1686 - 1690 Literal <nosymbol> true
42 44006 1642 - 1691 Apply org.geotools.referencing.CRS.findMathTransform org.geotools.referencing.CRS.findMathTransform(crs.user, crs.target, true)
42 44007 1613 - 1692 Apply org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer.setMathTransform transformer.setMathTransform(org.geotools.referencing.CRS.findMathTransform(crs.user, crs.target, true))
43 44008 1751 - 1761 Select org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems.target crs.target
43 44009 1717 - 1762 Apply org.geotools.feature.FeatureTypes.transform org.geotools.feature.FeatureTypes.transform(returnSft, crs.target)
44 44010 1862 - 1883 Apply org.geotools.api.feature.type.PropertyType.getUserData returnSft.getUserData()
44 44011 1820 - 1884 Apply org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable(transformed, returnSft.getUserData())
44 44012 1794 - 1898 Apply org.locationtech.geomesa.index.utils.Reprojection.TransformReprojection.<init> new Reprojection.this.TransformReprojection(org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable(transformed, returnSft.getUserData()), transformer)
45 44014 1926 - 1936 Select org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems.native crs.native
45 44015 1914 - 1936 Apply java.lang.Object.!= crs.user.!=(crs.native)
45 44021 1938 - 2131 Block <nosymbol> { val transformed: org.geotools.api.feature.simple.SimpleFeatureType = org.geotools.feature.FeatureTypes.transform(returnSft, crs.user); new Reprojection.this.UserReprojection(org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable(transformed, returnSft.getUserData())) }
45 44024 1910 - 2231 If <nosymbol> if (crs.user.!=(crs.native)) { val transformed: org.geotools.api.feature.simple.SimpleFeatureType = org.geotools.feature.FeatureTypes.transform(returnSft, crs.user); new Reprojection.this.UserReprojection(org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable(transformed, returnSft.getUserData())) } else throw new scala.`package`.IllegalArgumentException(scala.StringContext.apply("Trying to reproject to the same CRS: ", "").s(crs))
46 44016 1998 - 2006 Select org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems.user crs.user
46 44017 1964 - 2007 Apply org.geotools.feature.FeatureTypes.transform org.geotools.feature.FeatureTypes.transform(returnSft, crs.user)
47 44018 2102 - 2123 Apply org.geotools.api.feature.type.PropertyType.getUserData returnSft.getUserData()
47 44019 2060 - 2124 Apply org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable(transformed, returnSft.getUserData())
47 44020 2039 - 2125 Apply org.locationtech.geomesa.index.utils.Reprojection.UserReprojection.<init> new Reprojection.this.UserReprojection(org.locationtech.geomesa.utils.geotools.SimpleFeatureTypes.immutable(transformed, returnSft.getUserData()))
49 44022 2145 - 2225 Throw <nosymbol> throw new scala.`package`.IllegalArgumentException(scala.StringContext.apply("Trying to reproject to the same CRS: ", "").s(crs))
49 44023 2145 - 2225 Block <nosymbol> throw new scala.`package`.IllegalArgumentException(scala.StringContext.apply("Trying to reproject to the same CRS: ", "").s(crs))
67 44025 2786 - 2835 Apply org.geotools.api.feature.type.FeatureType.getGeometryDescriptor org.locationtech.geomesa.index.conf.QueryHints.RichHints(query.getHints()).getReturnSft.getGeometryDescriptor()
67 44036 2779 - 3222 Apply scala.Option.flatMap scala.Option.apply[org.geotools.api.feature.type.GeometryDescriptor](org.locationtech.geomesa.index.conf.QueryHints.RichHints(query.getHints()).getReturnSft.getGeometryDescriptor()).flatMap[org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems](((descriptor: org.geotools.api.feature.type.GeometryDescriptor) => { val native: org.geotools.api.referencing.crs.CoordinateReferenceSystem = descriptor.getCoordinateReferenceSystem(); val source: org.geotools.api.referencing.crs.CoordinateReferenceSystem = scala.Option.apply[org.geotools.api.referencing.crs.CoordinateReferenceSystem](query.getCoordinateSystem()).getOrElse[org.geotools.api.referencing.crs.CoordinateReferenceSystem](native); val target: org.geotools.api.referencing.crs.CoordinateReferenceSystem = scala.Option.apply[org.geotools.api.referencing.crs.CoordinateReferenceSystem](query.getCoordinateSystemReproject()).getOrElse[org.geotools.api.referencing.crs.CoordinateReferenceSystem](native); if (target.==(source).&&(source.==(native))) scala.None else scala.Some.apply[org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems](Reprojection.this.QueryReferenceSystems.apply(native, source, target)) }))
68 44026 2882 - 2921 Apply org.geotools.api.feature.type.GeometryDescriptor.getCoordinateReferenceSystem descriptor.getCoordinateReferenceSystem()
69 44027 2943 - 2994 Apply scala.Option.getOrElse scala.Option.apply[org.geotools.api.referencing.crs.CoordinateReferenceSystem](query.getCoordinateSystem()).getOrElse[org.geotools.api.referencing.crs.CoordinateReferenceSystem](native)
70 44028 3016 - 3076 Apply scala.Option.getOrElse scala.Option.apply[org.geotools.api.referencing.crs.CoordinateReferenceSystem](query.getCoordinateSystemReproject()).getOrElse[org.geotools.api.referencing.crs.CoordinateReferenceSystem](native)
71 44029 3109 - 3125 Apply java.lang.Object.== source.==(native)
71 44030 3089 - 3125 Apply scala.Boolean.&& target.==(source).&&(source.==(native))
71 44031 3129 - 3133 Select scala.None scala.None
71 44032 3129 - 3133 Block scala.None scala.None
72 44033 3158 - 3203 Apply org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems.apply Reprojection.this.QueryReferenceSystems.apply(native, source, target)
72 44034 3153 - 3204 Apply scala.Some.apply scala.Some.apply[org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems](Reprojection.this.QueryReferenceSystems.apply(native, source, target))
72 44035 3153 - 3204 Block scala.Some.apply scala.Some.apply[org.locationtech.geomesa.index.utils.Reprojection.QueryReferenceSystems](Reprojection.this.QueryReferenceSystems.apply(native, source, target))
87 44037 3652 - 3673 Apply org.geotools.api.feature.simple.SimpleFeatureType.getAttributeCount TransformReprojection.this.sft.getAttributeCount()
87 44041 3637 - 3816 ApplyToImplicitArgs scala.Array.tabulate scala.Array.tabulate[Object](TransformReprojection.this.sft.getAttributeCount())(((i: Int) => feature.getAttribute(i) match { case (g @ (_: org.locationtech.jts.geom.Geometry)) => TransformReprojection.this.transformer.transform(g) case (a @ _) => a }))((ClassTag.Object: scala.reflect.ClassTag[Object]))
89 44038 3752 - 3776 Apply org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer.transform TransformReprojection.this.transformer.transform(g)
89 44039 3752 - 3776 Block org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer.transform TransformReprojection.this.transformer.transform(g)
90 44040 3797 - 3798 Ident org.locationtech.geomesa.index.utils.Reprojection.TransformReprojection.a a
93 44042 3846 - 3849 Select org.locationtech.geomesa.index.utils.Reprojection.TransformReprojection.sft TransformReprojection.this.sft
93 44043 3851 - 3864 Apply org.geotools.api.feature.simple.SimpleFeature.getID feature.getID()
93 44044 3874 - 3893 Apply org.geotools.api.feature.Property.getUserData feature.getUserData()
93 44045 3823 - 3894 Apply org.locationtech.geomesa.features.ScalaSimpleFeature.<init> new org.locationtech.geomesa.features.ScalaSimpleFeature(TransformReprojection.this.sft, feature.getID(), values, feature.getUserData())
103 44046 4227 - 4230 Select org.locationtech.geomesa.index.utils.Reprojection.UserReprojection.sft UserReprojection.this.sft
103 44047 4203 - 4240 Apply org.locationtech.geomesa.features.ScalaSimpleFeature.copy org.locationtech.geomesa.features.ScalaSimpleFeature.copy(UserReprojection.this.sft, feature)