Skip to main content

The code always knows ... how to reproject

Posted by jive on February 22, 2007 at 5:37 PM PST




http-equiv="content-type">
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.

cellpadding="2" cellspacing="2">
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:

cellpadding="2" cellspacing="2">
if (forcedCS != null)
    results = new ForceCoordinateSystemFeatureResults(results, forcedCS);
if (reprojectCS != null)
    results = new ReprojectFeatureResults(results, reprojectCS);


The Review

In  href="http://weblogs.java.net/blog/jive/archive/2007/02/the_code_always.html">part
one we applied the example that changed the FeatureType, now
we need to look at reprojecting the data.



In href="http://weblogs.java.net/blog/jive/archive/2007/02/the_code_always_1.html">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  style="font-weight: bold;">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

cellpadding="2" cellspacing="2">
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:

cellpadding="2" cellspacing="2">
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  ...

border="1" cellpadding="2" cellspacing="2">
    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:

cellpadding="2" cellspacing="2">
        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():

cellpadding="2" cellspacing="2">
    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:

cellpadding="2" cellspacing="2">
    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.