ThinkGeo.com    |     Documentation    |     Premium Support

When is projection applied?

If I have a feature source in EPSG 4326, and my map is to be displayed in EPSG 26915 (UTM NAD83, Zone 15N), a transformation is executed.    At what point in the drawing process is the transformation applied?    It appears that I must build the projection as a property of the FeatureSource, and set the external SRID to the 26915.   In my custom FeatureSource, I build the requested Feature collection with the 4326 projection.   But when drawn on the map, that are clearly transformed into 26915.   Magic happened somewhere, but I'm curious as to where it happened.


I'm also surprised that I have to set the projection on the FeatureSource.    Logically, that prevents me from using the same data for a map defined in 26915 and in 26916, and, yes, we do that sometimes because we have to match the selected background imagery source.    It seems like the transformation should be a function of the layer, rather than of the data source.


So, as I understand it now... when I want to add a layer to a map, it is my responsibility to go to its data source and set its external projection to that of the map.   And if I want to change the SRID of the map, then I need to iterate every layer and change the external SRID of the layers datasource?


Is creating a projection expensive?   I'm surprised you don't have a way to define the target project on the map (as an object), and pass that into the Draw methods of the layer (or have it as a GeoCanvas property) such that the map display is fully decoupled from the data source.    You've had good reasons for any other design feature I've questioned.   I look forward to the reasons for this design (or a correction to my understanding of the process :))


Thanks!



Ted, 
  
   Be warned, this is a long one but I hope this grants you some more insight into our API and design methodology.   
  
 Skip if you are 100% sure of how this works. 
  
 _+_+_+_+_+_+_+_+_+_+_+ 
  
   The projection occurred in the concrete version of our methods.  Before we go too much further I want to give a brief overview of the whole Core and Concrete method system.  You may already know this but I want to make sure as it is key. 
  
   In our API there are a number of protected virtual method ending with Core.  Each one of these methods is paired up with another method we call a concrete method that is simply public and not virtual.  These methods have the same name as the virtual ones but without the Core and these are what the typical developer sees when not inheriting from our classes.  Typically the concrete method does some parameter checking and then calls the associated Core method.   
  
   This design allows us to accomplish a number of things.  First it allows you the developer to override Core methods and it is obvious which public methods they relate to.  The second is that is give us, the framework writers, a degree of freedom if you decide to override our Core methods.  For example if there are things we always want to happen on the Open method then we can add code there.  You can choose to override the OpenCore with your own implementation however you cannot replace our code in Open.  An example of this is on the Open method we check to see if it is already open and if so we never call the OpenCore.  We could also after the fact add an Opening and Opened event even if you override the OpenCore.  The third reason is to allow you to focus on the main purpose of the override and not with plumbing details.  It also help you as a developer get the override correct.  If we always raise the event for you then you don’t have to remember to raise it etc. 
  
 _+_+_+_+_+_+_+_+_+_+_+ 
  
   In the case of the FeatureSource however we add extra code in our concrete methods to handle projection for you.  The reason we added it in the concrete methods is to shield you from the plumbing code of doing the projection.  If you create your own FeatureSource you never have to worry about projection because we take care of that, in the virtual methods you just need to focus on getting data from the source. If you wanted to use your own custom projection you inherit from the Projection class and write your custom code there.  Some other things we shield you from is editing buffers, if you are in a transaction and have the IsLiveTransaction set to true we will behind the scenes integrate all of the pending transaction feature into your method calls for you. 
  
   We wanted to do the projection as close to the data source as possible.  The reason for this is that outside of the FeatureSource we wanted the data to always in the same projection as the map.  You as a developer never have to wonder if the data is in its native projection or the projected version when writing extensions.  The vision was that for the developer the FeatureSource is like a shell and it makes all of the layers have the same projection in the system.   
  
   Imagine if we didn’t do this and you wrote a custom Style or GeoCanvas and you never knew the projection of what was coming in. We also wanted to isolate projection to un-clutter APIs.  Imagine again that we did the projection in the GeoCanvas at the end.  That means the projection would need to be passed through the Layer.Draw, and then through the Style.Draw etc.  At every turn developers overriding would wonder why they needed that projection object, was it their duty to do the projection they might ask.  In the end if we did it very close to the data itself we sidestep so many issues. 
  
   We also need to remember that projection goes way beyond drawing and if we did it in the GeoCanvas then what about spatial queries, getting the bounding box for the Layer, adding or editing shapes.  Currently when you set the Projection property in the FeatureSource you can pretend that the data inside is really the projected data.  For example, imagine you have some point data in decimal degrees as the source and you use the Projection property to project it to UTM for display.  Then the user clicks on the map and you want to add that point to the FeatureSource.  You can take the UTM point and simply add that to the FeatureSource as UTM.  Behind the scenes we know the data is stored in decimal degrees and we will do a reverse projection on it to get it back to the form the underlying data source needs.  In this way there is no need for you to do anything.  To accomplish this the projection has to be at the lowest levels and really close to the underlying data store.    
  
   Another reason why we added the Projection to the FeatureSource  is due to our ‘pieces and parts’ design goal…  When we deigned the 3.x framework we didn’t design it from a map viewer down.  Instead we started with independent fully usable pieces and built a map viewer from it.  The FeatureSource was designed to be a usable self contained class in its own right outside of the map viewer context.  There were lots of scenarios we had where we used the pieces of Map Suite without ever rending a map at all.  Since we took the pieces approach I think our API is a lot more flexible and can be molded fairly easily.  There are many users who like this as when they want to do something special that we do not support at a high level they can re-use our pieces and they quickly build up a new implementation.  We try and stay away from designing from the top down.  We tend to imagine from the top down and then design from the bottom up.  I hope that makes sense. 
  
   That is the background of the design however you make a very good point.  I think that is is helpful to be able to set a map projection and specify the projection of all of the layers and have some mechanism to work everything out.  This of course assumes that all of the projections are using the same library.  In our case we allow the flexibility for each FeatureSource to use its own projection and that could be a user written one, such as as rotation or perspective. 
  
   If we can make some assumptions about a default projection engine such as Proj4 I think we could come up with some high level APIs to allow the automation you are talking about and also have the flexibility of using your own projections.  We need to give this some thought t make sure it is easy and useful.  We also need to make sure it doesn’t complicate things even further. :-) 
  
 David

Thank you for the explanation. So, when I implement a custom FeatureSource object, I override GetAllFeaturesCore and return a collection of Feature structures, with the WKB in my native projection... 4326. In my client, if I execute a GetAllFeatures call against this FeatureSource, the resultant Feature collection will have Feature structures with a WKB in 26915 if I have assigned a 4326 -> 26915 projection to the FeatureSource? 



The Proj4 engine is different than the open source Proj.Net effort? And it doesn't implement the projection interfaces of GeoAPI? 



I become totally confused when talking about projections. It seems that that in some systems, there are coordinate systems, projections, and transformations. 4326 and 26915 would be projectsions, and the conversion from 4326 to 26915 would be a transformation. But in your framework, the conversion from 4326 to 26915 is called a projection? As near as I can tell, in the Proj.Net and OGC architecture, you build a transformation from two projection objects... so you define the FeatureSource in the 4326 projection, and the map in the 26915 projection, and as these projections implement an IProjection interface, at any point in the process the two projections get joined into a transform? 



I'm rusty on this. I learn enough to accomplish my goals in any application that I work with... usually fairly early on, and then it "just works" from there forward. That's where I'm at on MapSuite... building the framework that will let my data be displayed in the user's preferred "projection". I'm accomplishing that by cascading an "ExternalSRID" value down from the map into all layers, and utltimately their feature sources. 



This discussion has centered on the FeatureSource. So if I inherit from a Layer, rather than from a FeatureLayer, It is my responsibility to have my data in the project projection prior to calling any of the Draw functions? ie... any data being drawn onto a GeoCanvas, at any point, must be in the target projection, right? 



Thanks again for the excellent explanation.



Ted, 
  
   You are correct in that if you set the projection property on your own FeatureSource then it will return point in the 26915 even though in the GetAllFeaturesCore you retrieved them as 4326. 
  
   Proj4 is a C based projection library.  We support it with a wrapper class called Proj4Projection.  Proj.Net is another project that is a less than exact port of Proj4.  We have it as an extension as it has an extra .net dependency.  I am not sure what implements the GeoAPI interface.  We do not use that at the moment.  What I am sure of is that Proj4 is much more capable than Proj.Net and is faster so we bundle that in by default.  As I also mentioned we support Proj.Net but it is an extension that ships Map Suite but you have to reference it separately. 
  
   I think this part shows that our original background is more software engineering than GIS.  I think the terms you are using are the correct ones and our Projection is probably not the right term.  So far sa I know you are the only person to bring it up in 5+ years, congratulations! :-)  In the end the reason why is just as you said, people want to go from X to Y projection and know even less about projection than you or I anyway. 
  
   I will jot this down and in the next incarnation we can get this right.  We can deprecate Projection and so something better. 
  
   If you inherit from Layer you can do whatever you want to do but trade quite a bit.  You are correct in that it needs to be in a unified projection as no projection happens up stream…  Another thing to consider is if you inherit from Layer directly and you want to use ZoomLevels then you can always add a ZoomLevels property to your Layer.  Working with ZoomLevels is not too hard and off of that class you have all of your styles etc.   
  
 You might want to carefully think about what you are trading in by not using FeatureSource.  I am not sure why you are considering not using it.  Of course if you want to do your own projection just do not populate the Projection property and add your own on your custom FeatureSource.  You also should consider you will give up editing if you do not use FeatureLayer as well.  I want to make sure you make the right call early in this process. 
  
 On other quick thing.  If you source of data offers some kind of spatial index or optimized search based on a bounding box make sure you override the GetFeaturesInBoundingBoxCore.  This is the main method called by all the spatial queries.  If you only override the GetAllFeaturesCore then we will always select all records and iterate them to verify if the meet the spatial query rules.  Just an FYI. 
  
 David

Thank you for the clarfications.    I’ve inherited from layer for some base data implementations where I want to display multiple layers.  Thought I would skip dummying up a FeatureSource, but my layer implementation has a collection of FeatureLayers.   This is read-only base data. 
  
 Good point on the ability to edit.     Still exploring all options, and making sure I understand the pros and cons of different approaches. 
  
 Over the weekend, I implemented a different layer that wraps two FeatureLayers…  One of the layers is vector point data, and there is an optional layer that is the matching surface.   I implemented the logic for user choices about whether to display the surface and then display the source site locations on top of the surface if it is displayed all within the class.  The encapsulation of this is much cleaner than prior code.   However, you are making me think that I should likely derive this layer from a FeatureLayer, and just use the FeatureSource of my source site layer as the required source for the aggregation layer. 
  
 Your last paragraph brings up an interesting point…   If I have 10,000 features (polygon cells on a surface layer), and I return those in the GetFeaturesInBoundingBoxCore, you trust me to have done the spatial filtering, right?   You don’t filter them again in your concrete implementation do you? 
  
 Thanks again for the great discussion. 
  


Ted, 
  
  Sounds like you are building quite the rocket ship.  When you start to aggregate layers and all kinds of stuff then you are outside of my realm and into your requirements. :-) 
  
   You are right, we will not do any spatial filtering on what you return in the GeFeaturesByBoundingBoxCore or whatever it is called.  The concrete methods try not to trample on what the method does but just decorate it up, dot the I’s and cross the T’s.   
  
   One thing we might do is if you were editing then we might remove or add records in accordance with what is in the edit buffer.  You might have an update that is pending and in your source data it is in one spot but our edit buffer says it is in another.  In that case we will fiddle with the results to make sure they are right. 
  
 David