Skip to content

Commit e4ac550

Browse files
committed
Add pagination for Layers that natively support it and simulated sorting for Layers that don't support sorting natively. Simulated pagination will be available when we switch to GeoTools 9.0
1 parent 8e46a52 commit e4ac550

File tree

3 files changed

+203
-13
lines changed

3 files changed

+203
-13
lines changed

src/main/groovy/geoscript/layer/Cursor.groovy

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import org.geotools.feature.FeatureIterator
55
import org.opengis.feature.simple.SimpleFeature
66
import org.opengis.feature.simple.SimpleFeatureType
77
import org.geotools.feature.FeatureCollection
8+
import org.geotools.data.store.MaxFeaturesIterator
9+
import org.geotools.data.sort.SortedFeatureIterator
10+
import org.opengis.filter.sort.SortBy
811

912
/**
1013
* A Cursor is a Iterator over a Feature objects.
@@ -30,32 +33,57 @@ class Cursor implements Iterator {
3033
* The GeoTools FeatureIterator
3134
*/
3235
private FeatureIterator<SimpleFeature> iter
33-
36+
3437
/**
3538
* The GeoTools FeatureCollection
3639
*/
3740
FeatureCollection<SimpleFeatureType, SimpleFeature> col
3841

42+
/**
43+
* A Map of options. Options can be: sort, start, max
44+
*/
45+
private Map options
46+
3947
/**
4048
* Create a new Cursor with a FeatureCollection
49+
* @param options A Map of options (sort, start, max)
4150
* @param col The GeoTools FeatureCollection
4251
*/
43-
Cursor(FeatureCollection<SimpleFeatureType, SimpleFeature> col) {
52+
Cursor(Map options = [:], FeatureCollection<SimpleFeatureType, SimpleFeature> col) {
53+
this.options = options
4454
this.col = col
45-
this.iter = col.features()
55+
createIterator()
4656
}
4757

4858
/**
4959
* Create a new Cursor with a FeatureCollection and a Layer
60+
* @param options A Map of options (sort, start, max)
5061
* @param col The GeoTools FeatureCollection
5162
* @param layer The Geoscript Layer
5263
*/
53-
Cursor(FeatureCollection<SimpleFeatureType, SimpleFeature> col, Layer layer) {
64+
Cursor(Map options = [:], FeatureCollection<SimpleFeatureType, SimpleFeature> col, Layer layer) {
65+
this.options = options
5466
this.col = col
55-
this.iter = col.features()
67+
createIterator()
5668
this.layer = layer
5769
}
5870

71+
/**
72+
* Create the FeatureIterator based on the FeatureCollection and options
73+
*/
74+
protected void createIterator() {
75+
this.iter = col.features()
76+
if (options.containsKey("sort")) {
77+
this.iter = new SortedFeatureIterator(this.iter, col.schema, options.sort as SortBy[], Integer.MAX_VALUE)
78+
}
79+
// This will work in GeoTools 9.0
80+
/*if (options.containsKey("start") && options.containsKey("max")) {
81+
long start = options.start as long
82+
long end = start + options.max as long
83+
this.iter = new MaxFeaturesIterator<SimpleFeature>(this.iter, start, end)
84+
}*/
85+
}
86+
5987
/**
6088
* Get the next Feature
6189
* @return The next Feature
@@ -110,6 +138,6 @@ class Cursor implements Iterator {
110138
* Reset and read the Features again.
111139
*/
112140
void reset() {
113-
iter = col.features()
141+
createIterator()
114142
}
115143
}

src/main/groovy/geoscript/layer/Layer.groovy

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class Layer {
8787
/**
8888
* The FilterFactory2 for creating Filters
8989
*/
90-
private final static FilterFactory2 filterFactory = org.geotools.factory.CommonFactoryFinder.getFilterFactory2(org.geotools.factory.GeoTools.getDefaultHints())
90+
protected final static FilterFactory2 filterFactory = org.geotools.factory.CommonFactoryFinder.getFilterFactory2(org.geotools.factory.GeoTools.getDefaultHints())
9191

9292
/**
9393
* Create a new Layer from a GeoTools FeatureSource
@@ -418,16 +418,55 @@ class Layer {
418418
return features
419419
}
420420

421+
/**
422+
* Get a Cursor over the Features of the Layer using named parameters.
423+
* @param options. The Map of named parameters can include:
424+
* <ul>
425+
* <li>filter = The Filter or Filter String to limit the Features. Defaults to null.</li>
426+
* <li>sort = A List of Lists that define the sort order [[Field or Field name, "ASC" or "DESC"],...]. Not all Layers
427+
* support sorting!</li>
428+
* <li>max= The maximum number of Features to include in the Cursor</li>
429+
* <li>start = The index of the record to start the cursor at. Together with maxFeatures this simulates paging.
430+
* Not all Layers support the start index and paging!</li>
431+
* </ul>
432+
* @return A Cursor
433+
*/
434+
Cursor getCursor(Map options) {
435+
getCursor(options.get("filter", null), options.get("sort", null),
436+
options.get("max",-1), options.get("start", -1))
437+
}
438+
421439
/**
422440
* Get a Cursor over the Features of the Layer.
423-
* @param filer The Filter or Filter String to limit the Features. Defaults to null.
441+
* @param filter The Filter or Filter String to limit the Features. Defaults to null.
424442
* @param sort A List of Lists that define the sort order [[Field or Field name, "ASC" or "DESC"],...]. Not all Layers
425443
* support sorting!
444+
* @param max The maximum number of Features to include in the Cursor
445+
* @param start The index of the record to start the cursor at. Together with maxFeatures this simulates paging.
446+
* Not all Layers support the start index and paging!
426447
* @return A Cursor
427448
*/
428-
Cursor getCursor(def filter = null, List sort = null) {
449+
Cursor getCursor(def filter = null, List sort = null, int max = -1, int start = -1) {
450+
Map cursorOptions = [:]
429451
Filter f = (filter == null) ? Filter.PASS : new Filter(filter)
430452
DefaultQuery q = new DefaultQuery(getName(), f.filter)
453+
if (max > -1) {
454+
q.maxFeatures = max
455+
}
456+
if (start > -1) {
457+
if (fs.queryCapabilities.offsetSupported) {
458+
q.startIndex = start
459+
} else {
460+
throw new UnsupportedOperationException("Start and max are not supported by this layer!");
461+
// This will work in GeoTools 9.0
462+
// cursorOptions.start = start
463+
// cursorOptions.max = max
464+
// Reset max features because the we will
465+
// be using the MaxFeaturesIterator in Cursor
466+
// and it needs all of the Features to simulate paging
467+
// q.maxFeatures = Integer.MAX_VALUE
468+
}
469+
}
431470
if (getProj()) {
432471
q.coordinateSystem = getProj().crs
433472
}
@@ -445,11 +484,11 @@ class Layer {
445484
if (fs.queryCapabilities.supportsSorting(sortByArray)) {
446485
q.sortBy = sortByArray
447486
} else {
448-
println "This Layer does not support sorting!"
487+
cursorOptions.sort = sortByArray
449488
}
450489
}
451490
def col = fs.getFeatures(q)
452-
return new Cursor(col, this)
491+
return new Cursor(cursorOptions, col, this)
453492
}
454493

455494
/**

src/test/groovy/geoscript/layer/LayerTestCase.groovy

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import geoscript.filter.Filter
1010
import geoscript.workspace.Memory
1111
import geoscript.geom.*
1212
import geoscript.workspace.Workspace
13+
import geoscript.workspace.H2
1314

1415
/**
1516
* The Layer UnitTest
@@ -358,31 +359,153 @@ class LayerTestCase {
358359
}
359360

360361
@Test void cursorSorting() {
362+
File f = new File("target/h2").absoluteFile
363+
if (f.exists()) {
364+
boolean deleted = f.deleteDir()
365+
}
366+
H2 h2 = new H2("facilities", "target/h2")
367+
Layer layer = h2.create('facilities',[new Field("geom","Point", "EPSG:2927"), new Field("name","string"), new Field("price","float")])
368+
layer.add(new Feature(["geom": new Point(111,-47), "name": "A", "price": 10], "house1"))
369+
layer.add(new Feature(["geom": new Point(112,-46), "name": "B", "price": 12], "house2"))
370+
layer.add(new Feature(["geom": new Point(113,-45), "name": "C", "price": 13], "house3"))
371+
layer.add(new Feature(["geom": new Point(113,-45), "name": "D", "price": 14], "house4"))
372+
layer.add(new Feature(["geom": new Point(113,-45), "name": "E", "price": 15], "house5"))
373+
layer.add(new Feature(["geom": new Point(113,-45), "name": "F", "price": 16], "house6"))
374+
375+
Cursor c = layer.getCursor(Filter.PASS, [["name","ASC"]])
376+
assertEquals "A", c.next()["name"]
377+
assertEquals "B", c.next()["name"]
378+
assertEquals "C", c.next()["name"]
379+
assertEquals "D", c.next()["name"]
380+
assertEquals "E", c.next()["name"]
381+
assertEquals "F", c.next()["name"]
382+
c.close()
383+
384+
c = layer.getCursor(Filter.PASS, ["name"])
385+
assertEquals "A", c.next()["name"]
386+
assertEquals "B", c.next()["name"]
387+
assertEquals "C", c.next()["name"]
388+
assertEquals "D", c.next()["name"]
389+
assertEquals "E", c.next()["name"]
390+
assertEquals "F", c.next()["name"]
391+
c.close()
392+
393+
c = layer.getCursor(Filter.PASS, [["name","DESC"]])
394+
assertEquals "F", c.next()["name"]
395+
assertEquals "E", c.next()["name"]
396+
assertEquals "D", c.next()["name"]
397+
assertEquals "C", c.next()["name"]
398+
assertEquals "B", c.next()["name"]
399+
assertEquals "A", c.next()["name"]
400+
c.close()
401+
402+
// Named Parameters
403+
c = layer.getCursor(filter: "price >= 14.0", sort: [["price", "DESC"]])
404+
assertTrue c.hasNext()
405+
assertEquals "F", c.next()["name"]
406+
assertEquals "E", c.next()["name"]
407+
assertEquals "D", c.next()["name"]
408+
assertFalse c.hasNext()
409+
c.close()
410+
411+
h2.close()
412+
}
413+
414+
@Test void cursorSortingAndPagingWithUnsupportedLayer() {
361415
Schema s = new Schema("facilities", [new Field("geom","Point", "EPSG:2927"), new Field("name","string"), new Field("price","float")])
362416
Layer layer = new Layer("facilities", s)
363417
layer.add(new Feature([new Point(111,-47), "A", 10], "house1", s))
364418
layer.add(new Feature([new Point(112,-46), "B", 12], "house2", s))
365419
layer.add(new Feature([new Point(113,-45), "C", 11], "house3", s))
420+
layer.add(new Feature([new Point(113,-44), "D", 15], "house4", s))
366421

422+
// Sort ascending explicitly
367423
Cursor c = layer.getCursor(Filter.PASS, [["name","ASC"]])
368424
assertEquals "A", c.next()["name"]
369425
assertEquals "B", c.next()["name"]
370426
assertEquals "C", c.next()["name"]
427+
assertEquals "D", c.next()["name"]
428+
assertFalse c.hasNext()
371429
c.close()
372430

431+
// Sort ascending implicitly
373432
c = layer.getCursor(Filter.PASS, ["name"])
374433
assertEquals "A", c.next()["name"]
375434
assertEquals "B", c.next()["name"]
376435
assertEquals "C", c.next()["name"]
436+
assertEquals "D", c.next()["name"]
437+
assertFalse c.hasNext()
377438
c.close()
378439

379-
// @TODO MemoryDataStore doesn't actually sort!
380-
/*c = layer.getCursor(Filter.PASS, [["name","DESC"]])
440+
// Sort descending
441+
c = layer.getCursor(Filter.PASS, [["name","DESC"]])
442+
assertEquals "D", c.next()["name"]
381443
assertEquals "C", c.next()["name"]
382444
assertEquals "B", c.next()["name"]
383445
assertEquals "A", c.next()["name"]
446+
assertFalse c.hasNext()
447+
c.close()
448+
449+
// Page (will work with GeoTools 9.0)
450+
/*c = layer.getCursor(start:0, max:2)
451+
assertEquals "A", c.next()["name"]
452+
assertEquals "B", c.next()["name"]
453+
assertFalse c.hasNext()
454+
c.close()
455+
c = layer.getCursor(start:2, max:2)
456+
assertEquals "C", c.next()["name"]
457+
assertEquals "D", c.next()["name"]
458+
assertFalse c.hasNext()
459+
c.close()
460+
c = layer.getCursor("price > 10", [["price", "DESC"]], 2, 1)
461+
assertEquals "B", c.next()["name"]
462+
assertEquals "C", c.next()["name"]
463+
assertFalse c.hasNext()
384464
c.close()*/
385465
}
386466

467+
@Test void cursorPaging() {
468+
File f = new File("target/h2").absoluteFile
469+
if (f.exists()) {
470+
boolean deleted = f.deleteDir()
471+
}
472+
H2 h2 = new H2("facilities", "target/h2")
473+
Layer layer = h2.create('facilities',[new Field("geom","Point", "EPSG:2927"), new Field("name","string"), new Field("price","float")])
474+
layer.add(new Feature(["geom": new Point(111,-47), "name": "A", "price": 10], "house1"))
475+
layer.add(new Feature(["geom": new Point(112,-46), "name": "B", "price": 12], "house2"))
476+
layer.add(new Feature(["geom": new Point(113,-45), "name": "C", "price": 13], "house3"))
477+
layer.add(new Feature(["geom": new Point(113,-45), "name": "D", "price": 14], "house4"))
478+
layer.add(new Feature(["geom": new Point(113,-45), "name": "E", "price": 15], "house5"))
479+
layer.add(new Feature(["geom": new Point(113,-45), "name": "F", "price": 16], "house6"))
480+
481+
Cursor c = layer.getCursor(Filter.PASS, [["name","ASC"]], 2, 0)
482+
assertEquals "A", c.next()["name"]
483+
assertEquals "B", c.next()["name"]
484+
assertFalse c.hasNext()
485+
c.close()
486+
487+
c = layer.getCursor(Filter.PASS, [["name","ASC"]], 2, 2)
488+
assertEquals "C", c.next()["name"]
489+
assertEquals "D", c.next()["name"]
490+
assertFalse c.hasNext()
491+
c.close()
492+
493+
c = layer.getCursor(Filter.PASS, [["name","ASC"]], 2, 4)
494+
assertEquals "E", c.next()["name"]
495+
assertEquals "F", c.next()["name"]
496+
assertFalse c.hasNext()
497+
c.close()
498+
499+
// Named parameters
500+
c = layer.getCursor(start: 0, max: 4)
501+
assertEquals "A", c.next()["name"]
502+
assertEquals "B", c.next()["name"]
503+
assertEquals "C", c.next()["name"]
504+
assertEquals "D", c.next()["name"]
505+
c.close()
506+
507+
h2.close()
508+
}
509+
387510
}
388511

0 commit comments

Comments
 (0)