The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


The code always knows ... how to reproject

Posted by jive on February 22, 2007 at 5:37 PM PST
The code knows .. how to reproject data Part three on the road to support reprojection as part of the GeoTools Query API, now that the FeatureType is all straigtened out lets change the actual information to match.

To Review

The GeoTools API has fallen into a trap where we promissed to reproject data when asked the correct Query. Problem is none of the implementations picked up on this change. Here is what we want to make possible.
CoordinateReferenceSystem world = CRS.decode("EPSG:4326"); // world lon/lat
CoordinateReferenceSystem local = CRS.decode("EPSG:3005"); // british columbia

FeatureSource road = store.getFeatureSource( "road" );

DefaultQuery query = new DefaultQuery( "road", Filter.INCLUDE, new String[]{ "geom", "name" } );
query.setCoordinateSystem( local );
query.setCoordinateSystemReproject( world );

FeatureCollection features = road.getFeatures( query );

Since this is needed functionality the developer community quickly hacked around the problem, leaving us with several working examples to grab the right answer from.

Here is the "fix" for reprojecting the data:
if (forcedCS != null)
results = new ForceCoordinateSystemFeatureResults(results, forcedCS);
if (reprojectCS != null)
results = new ReprojectFeatureResults(results, reprojectCS);

The Review

In part one we applied the example that changed the FeatureType, now we need to look at reprojecting the data.

In part two we got distracted by some inconsistent error handling ... and made a choice between fail siliently, fail with an exception and doing the right thing. In this case it was easy - the query is experssed using set theory and we made a nice empty set.

Returning to our origional problem  we had test failure when checking to see that the FeatureType in fact changed - turns out I was testing the wrong thing:
  • FeatureCollection.getFeatureType() - returns the type of a the collection. Surprisingly collections are features as well. Makes sense if you consider that a feature is something that can be drawn on a map.
  • FeatureCollection.getSchema() - is the type of the contents of the collection
Bah! When testing the FeatureCollection.getSchema() we found that that our FeatureType has indeed changed.

Looking at the Solution

The two utility classes employed by the "fix" are pretty straight forward:
  • ForceCoordinateSystemFeatureResults - this one just changes the FeatureType ... something we have already accomplished.
  • ReprojectFeaureResults - takes an existing FeatureCollection and grinds through the data applying a MathTransform to each Geometry
Lets look at the part that does the magic: ReprojectFeatureIterator
Feature next = reader.next();
Object[] attributes = next.getAttributes(null);

for (int i = 0; i < attributes.length; i++) {
if (attributes[i] instanceof Geometry) {
attributes[i] = transformer.transform((Geometry) attributes[i]);
}
}
return schema.create(attributes, next.getID());

Well that is pretty clear; grab the next Feature suck the attributes out into an array. For each Geometry use a MathTransform to reproject. And then use the FeatureType to create a new Feature with the results.

The MathTransform is produced using a utility class:
this.transform = CRS.findMathTransform(originalCs,destinationCS, true);

It sure would be nice if implementors made an optimized version avaialble, I will do this for PropertyDataStore as the last step.

Applying the Solution .. to AbstractFeatureSource? No

So we *could* just apply the wrapping classes as is  ...
    public FeatureCollection getFeatures(Query query) throws IOException {
FeatureType schema = getSchema();
String typeName = schema.getTypeName();

if( query.getTypeName() == null ){ // typeName unspecified we will "any" use a default
DefaultQuery defaultQuery = new DefaultQuery(query);
defaultQuery.setTypeName( typeName );
}
else if ( !typeName.equals( query.getTypeName() ) ){
return new EmptyFeatureCollection( schema );
}
FeatureCollection collection = new DefaultFeatureResults(this, query);
if( collection.getDefaultGeometry() == null ){
return collection; // no geometry no reprojection needed
}
if ( query.getCoordinateSystem() != null ){
collection = new ForceCoordinateSystemFeatureResults(collection, query.getCoordinateSystem() );
}
if ( query.getCoordinateSystemReproject() != null){
collection = new ReprojectFeatureResults(collection, query.getCoordinateSystemReproject() );
}
return collection;
}
That would work we would be done .. or would we?

Since we changed the FeatureType of DefaultFeaureResults (to reflect what should be happening) the traditional wrapping approach would be broken. When the ReprojectFeatureResults wrapper goes to produce a math transform it will see the expected CRS already ... and do nothing!

Since DefaultQueryResults is accepting a query I would like to see it do the work, let's get started.

Applying the Solution to DefaultFeatureResults

We need to pick up the correct MathTransform in the constructor (if we cannot do the work we may as well let them know early). Here is the code snip:
        CoordinateReferenceSystem origionalCRS = origionalType.getDefaultGeometry().getCoordinateSystem();
if( query.getCoordinateSystem() != null ){
origionalCRS = query.getCoordinateSystem();
}
try {
transform = CRS.findMathTransform( origionalCRS, cs, true);
} catch (FactoryException noTransform) {
throw (IOException) new IOException("Could not reproject data to "+cs).initCause( noTransform );
}

We are going to use the utility class GeometryCoordinateSequenceTransformer mentioned above to do the actual work.

The next part is to look at the code that does the reading .. getReader():
    public FeatureReader reader() throws IOException {
FeatureReader reader = featureSource.getDataStore().getFeatureReader(query,
getTransaction());

int maxFeatures = query.getMaxFeatures();
if (maxFeatures == Integer.MAX_VALUE) {
return reader;
} else {
return new MaxFeatureReader(reader, maxFeatures);
}
}

Oh look they already have a "wrapper" in place here ... MaxFeatureReader will cut off the feature supply when a specific max is reached.

Lets go shopping:
  • ReprojectFeatureIterator ... utility class used when your collection is memory based
  • ReprojectFeatureReader ... perfect!
Applying this is straight forward:
    public FeatureReader reader() throws IOException {
FeatureReader reader = featureSource.getDataStore().getFeatureReader(query,
getTransaction());

int maxFeatures = query.getMaxFeatures();
if (maxFeatures != Integer.MAX_VALUE) {
reader = new MaxFeatureReader(reader, maxFeatures);
}
if( transform != null ){
reader = new ReprojectFeatureReader( reader, schema, transform );
}
return reader;
}

That should be it ... getBounds uses reader() and so on.

So we have two solutions:
  • An example of how to support reprojection directly as part of your FeatureCollection (ie use of ReprojectFeatureReader or ReprojectFeatureIterator wrappers)
  • An example of how to support reprojection after the fact .. modify your getFeatures( Query method ) to  use ReprojectFeatureResults (or its colleciton based ReprojectingFeatureCollection)
It is very cool to get the reprojection as close to the data read as possible, creating Features, unpacking the attributes to reproject and then folding them back into Features again is a bit of a pain.

Almost done

Tomorrow we will try testing and see what sorts of problems this occurs in two real applications. If there is time we will review the javadocs that started this problem ... and explore ways of preventing this in the future.
Comments
Comments are listed in date ascending order (oldest first)