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.process.query
10 
11 import com.typesafe.scalalogging.LazyLogging
12 import org.geotools.api.data.{Query, SimpleFeatureSource}
13 import org.geotools.api.feature.Feature
14 import org.geotools.api.feature.simple.SimpleFeature
15 import org.geotools.data.collection.ListFeatureCollection
16 import org.geotools.data.simple.SimpleFeatureCollection
17 import org.geotools.process.factory.{DescribeParameter, DescribeProcess, DescribeResult}
18 import org.locationtech.geomesa.filter.factory.FastFilterFactory
19 import org.locationtech.geomesa.index.geotools.GeoMesaFeatureCollection
20 import org.locationtech.geomesa.index.process.GeoMesaProcessVisitor
21 import org.locationtech.geomesa.process.{FeatureResult, GeoMesaProcess}
22 import org.locationtech.geomesa.utils.collection.SelfClosingIterator
23 import org.locationtech.geomesa.utils.geotools.Conversions._
24 
25 @DescribeProcess(
26   title = "Geomesa-enabled Proximity Search",
27   description = "Performs a proximity search on a Geomesa feature collection using another feature collection as input"
28 )
29 class ProximitySearchProcess extends GeoMesaProcess with LazyLogging {
30 
31   @DescribeResult(description = "Output feature collection")
32   def execute(
33                @DescribeParameter(
34                  name = "inputFeatures",
35                  description = "Input feature collection that defines the proximity search")
36                inputFeatures: SimpleFeatureCollection,
37 
38                @DescribeParameter(
39                  name = "dataFeatures",
40                  description = "The data set to query for matching features")
41                dataFeatures: SimpleFeatureCollection,
42 
43                @DescribeParameter(
44                  name = "bufferDistance",
45                  description = "Buffer size in meters")
46                bufferDistance: java.lang.Double
47 
48                ): SimpleFeatureCollection = {
49 
50     logger.debug(s"Attempting Geomesa Proximity Search on collection type ${dataFeatures.getClass.getName}")
51 
52     val visitor = new ProximityVisitor(inputFeatures, dataFeatures, bufferDistance.doubleValue())
53     GeoMesaFeatureCollection.visit(dataFeatures, visitor)
54     visitor.getResult.results
55   }
56 }
57 
58 class ProximityVisitor(inputFeatures: SimpleFeatureCollection,
59                        dataFeatures: SimpleFeatureCollection,
60                        bufferInMeters: Double) extends GeoMesaProcessVisitor with LazyLogging {
61 
62   import org.locationtech.geomesa.filter.{ff, mergeFilters, orFilters}
63 
64   private val dwithin = {
65     val geomProperty = ff.property(dataFeatures.getSchema.getGeometryDescriptor.getName)
66     val geomFilters = SelfClosingIterator(inputFeatures.features).map { sf =>
67       ff.dwithin(geomProperty, ff.literal(sf.geometry), bufferInMeters, "meters")
68     }
69     orFilters(geomFilters.toSeq)
70   }
71 
72   // normally handled in our query planner, but we are going to use the filter directly here
73   private lazy val manualFilter = FastFilterFactory.optimize(dataFeatures.getSchema, dwithin)
74   private val manualVisitResults = new ListFeatureCollection(dataFeatures.getSchema)
75 
76   private var resultCalc = FeatureResult(manualVisitResults)
77 
78   // non-optimized visit
79   // here we use degrees for our filters since we are manually evaluating them.
80   override def visit(feature: Feature): Unit = {
81     val sf = feature.asInstanceOf[SimpleFeature]
82     if (manualFilter.evaluate(sf)) {
83       manualVisitResults.add(sf)
84     }
85   }
86 
87   override def getResult: FeatureResult = resultCalc
88 
89   override def execute(source: SimpleFeatureSource, query: Query): Unit = {
90     logger.debug(s"Running Geomesa Proximity Search on source type ${source.getClass.getName}")
91     val combinedFilter = mergeFilters(query.getFilter, dwithin)
92     resultCalc = FeatureResult(source.getFeatures(combinedFilter))
93   }
94 }
Line Stmt Id Pos Tree Symbol Tests Code
52 48549 2494 - 2522 Apply java.lang.Double.doubleValue bufferDistance.doubleValue()
52 48550 2444 - 2523 Apply org.locationtech.geomesa.process.query.ProximityVisitor.<init> new ProximityVisitor(inputFeatures, dataFeatures, bufferDistance.doubleValue())
53 48551 2528 - 2581 Apply org.locationtech.geomesa.index.geotools.GeoMesaFeatureCollection.visit org.locationtech.geomesa.index.geotools.GeoMesaFeatureCollection.visit(dataFeatures, visitor, org.locationtech.geomesa.index.geotools.GeoMesaFeatureCollection.visit$default$3)
54 48552 2586 - 2611 Select org.locationtech.geomesa.process.FeatureResult.results visitor.getResult().results
65 48553 2974 - 3026 Apply org.geotools.api.feature.type.PropertyDescriptor.getName ProximityVisitor.this.dataFeatures.getSchema().getGeometryDescriptor().getName()
65 48554 2962 - 3027 Apply org.geotools.api.filter.FilterFactory.property org.locationtech.geomesa.filter.`package`.ff.property(ProximityVisitor.this.dataFeatures.getSchema().getGeometryDescriptor().getName())
66 48555 3070 - 3092 Apply org.geotools.data.simple.SimpleFeatureCollection.features ProximityVisitor.this.inputFeatures.features()
66 48561 3050 - 3193 Apply org.locationtech.geomesa.utils.collection.CloseableIterator.map org.locationtech.geomesa.utils.collection.SelfClosingIterator.apply(ProximityVisitor.this.inputFeatures.features()).map[org.geotools.api.filter.spatial.DWithin](((sf: org.geotools.api.feature.simple.SimpleFeature) => org.locationtech.geomesa.filter.`package`.ff.dwithin(geomProperty, org.locationtech.geomesa.filter.`package`.ff.literal(org.locationtech.geomesa.utils.geotools.Conversions.RichSimpleFeature(sf).geometry), ProximityVisitor.this.bufferInMeters, "meters")))
67 48556 3148 - 3159 Select org.locationtech.geomesa.utils.geotools.Conversions.RichSimpleFeature.geometry org.locationtech.geomesa.utils.geotools.Conversions.RichSimpleFeature(sf).geometry
67 48557 3137 - 3160 Apply org.geotools.api.filter.FilterFactory.literal org.locationtech.geomesa.filter.`package`.ff.literal(org.locationtech.geomesa.utils.geotools.Conversions.RichSimpleFeature(sf).geometry)
67 48558 3162 - 3176 Select org.locationtech.geomesa.process.query.ProximityVisitor.bufferInMeters ProximityVisitor.this.bufferInMeters
67 48559 3178 - 3186 Literal <nosymbol> "meters"
67 48560 3112 - 3187 Apply org.geotools.api.filter.FilterFactory.dwithin org.locationtech.geomesa.filter.`package`.ff.dwithin(geomProperty, org.locationtech.geomesa.filter.`package`.ff.literal(org.locationtech.geomesa.utils.geotools.Conversions.RichSimpleFeature(sf).geometry), ProximityVisitor.this.bufferInMeters, "meters")
69 48562 3208 - 3225 Select scala.collection.TraversableOnce.toSeq geomFilters.toSeq
69 48563 3207 - 3207 Select org.locationtech.geomesa.filter.ff org.locationtech.geomesa.filter.`package`.ff
69 48564 3198 - 3226 ApplyToImplicitArgs org.locationtech.geomesa.filter.orFilters org.locationtech.geomesa.filter.`package`.orFilters(geomFilters.toSeq)(org.locationtech.geomesa.filter.`package`.ff)
74 48565 3480 - 3502 Apply org.geotools.feature.FeatureCollection.getSchema ProximityVisitor.this.dataFeatures.getSchema()
74 48566 3454 - 3503 Apply org.geotools.data.collection.ListFeatureCollection.<init> new org.geotools.data.collection.ListFeatureCollection(ProximityVisitor.this.dataFeatures.getSchema())
76 48567 3546 - 3564 Select org.locationtech.geomesa.process.query.ProximityVisitor.manualVisitResults ProximityVisitor.this.manualVisitResults
76 48568 3532 - 3565 Apply org.locationtech.geomesa.process.FeatureResult.apply org.locationtech.geomesa.process.FeatureResult.apply(ProximityVisitor.this.manualVisitResults)
81 48569 3734 - 3769 TypeApply scala.Any.asInstanceOf feature.asInstanceOf[org.geotools.api.feature.simple.SimpleFeature]
82 48570 3778 - 3803 Apply org.geotools.api.filter.Filter.evaluate ProximityVisitor.this.manualFilter.evaluate(sf)
82 48574 3774 - 3774 Literal <nosymbol> ()
82 48575 3774 - 3774 Block <nosymbol> ()
83 48571 3813 - 3839 Apply org.geotools.data.collection.ListFeatureCollection.add ProximityVisitor.this.manualVisitResults.add(sf)
83 48572 3835 - 3835 Literal <nosymbol> ()
83 48573 3813 - 3839 Block <nosymbol> { ProximityVisitor.this.manualVisitResults.add(sf); () }
87 48576 3893 - 3903 Select org.locationtech.geomesa.process.query.ProximityVisitor.resultCalc ProximityVisitor.this.resultCalc
91 48577 4115 - 4130 Apply org.geotools.api.data.Query.getFilter query.getFilter()
91 48578 4132 - 4139 Select org.locationtech.geomesa.process.query.ProximityVisitor.dwithin ProximityVisitor.this.dwithin
91 48579 4102 - 4140 Apply org.locationtech.geomesa.filter.mergeFilters org.locationtech.geomesa.filter.`package`.mergeFilters(query.getFilter(), ProximityVisitor.this.dwithin)
92 48580 4172 - 4206 Apply org.geotools.api.data.SimpleFeatureSource.getFeatures source.getFeatures(combinedFilter)
92 48581 4158 - 4207 Apply org.locationtech.geomesa.process.FeatureResult.apply org.locationtech.geomesa.process.FeatureResult.apply(source.getFeatures(combinedFilter))
92 48582 4145 - 4207 Apply org.locationtech.geomesa.process.query.ProximityVisitor.resultCalc_= ProximityVisitor.this.resultCalc_=(org.locationtech.geomesa.process.FeatureResult.apply(source.getFeatures(combinedFilter)))