Embedding the PROJ library in a Cocoa app

I just finished changes to use the PROJ cartographic projection library as an embedded framework in my current Cocoa (MacOS) project. This turns out to be one of those “obvious, once you know how” bits that I think is worth writing up for the next person who comes along.

I used William Kyngesburye’s MacOS Framework port of the PROJ library, available for binary download at http://www.kyngchaos.com/wiki/software:frameworks. But this port assumes you are installing the framework for the entire system, in /Library/Frameworks, which is not the case when you want to embed the framework in your app.

Wolf Rentzsch gives very detailed instructions for embedding Cocoa Frameworks, but those can’t be used here because the Kyngesburye port is apparently not built with Xcode and its source/compile insructions are not available. So I used install_name_tool instead:

(from http://www.cocoadev.com/index.pl?ApplicationLinking)

Changing the Install Name

Sometimes you’ll have a situation where you have a framework that’s designed to be installed somewhere special, but you want to make it embedded in your app. Maybe you don’t have the source code, or maybe you just don’t feel like rebuilding it, so you want to change its install name in place. Maybe you have an application with a bad library path in it, and you want to fix it without rebuilding it.

The install_name_tool command allows you to change both a library or framework’s install name, as well as the copied install names stored in an application, library, or framework. Use the -id flag to change a library’s install name, and use the -change flag to change a copied install name stored in an external binary.

The Xcode project

I added a “Copy Files” build phase to copy PROJ.framework into my target’s bundle, and I added this one-line “Run Script” build phase to my project:
install_name_tool -change /Library/Frameworks/PROJ.framework/Versions/4.5/PROJ @executable_path/../Frameworks/PROJ.framework/PROJ $BUILT_PRODUCTS_DIR/$TARGET_NAME.app/Contents/MacOS/$TARGET_NAME

This caused the PROJ framework to be correctly copied into the app, and changed its name so that references look in the app bundle, and not in the system library. But there’s more to do. PROJ has a set of files that define the geodesic parameters of each map projection. They are in the Resources/proj folder of the PROJ framework; I have to tell PROJ that the location of those files has also changed. That meant using the less common +init syntax of PROJ, instead of the +projname syntax commonly used on the Unix command line.

Getting this +init syntax to work properly took a little care. You need to find the proj folder at run time, then look up the correct projection file and projection identifier within that folder. I ended up wrapping PROJ in a separate Objective-C class, called MGProjection. Here is the relevant code:

@implementation MGProjection

+ (NSBundle *)projBundle
	return [NSBundle bundleWithPath:[[[NSBundle mainBundle] privateFrameworksPath] stringByAppendingPathComponent:@"PROJ.framework"]];

+ (NSString *)projDataDirectoryPath
	return [[self projBundle] pathForResource:@"proj" ofType:nil];

- (id) init
	self = [super init];
	[[MGProjection projBundle] load];
	[self becomePlatteCarre];
	return self;

- (void)becomePlatteCarre
	[self setProjInitFilename:@"epsg" key:32662];

- (void)setProjInitFilename:(NSString *)projFilename
	[self setProjInitString:
		[NSString stringWithFormat:@"+init=%@/%@:%d", 
			[MGProjection projDataDirectoryPath],

- (void)setProjInitString:(NSString *)aProjInitString
	projInitString = aProjInitString;
	pj = pj_init_plus([projInitString UTF8String]);

- (void)projectPointFrom:(NSPoint *)inPoint
					  to:(NSPoint *)outPoint
	projUV p;
	p.u = inPoint->x * DEG_TO_RAD;
	p.v = inPoint->y * DEG_TO_RAD;
	p = pj_fwd(p, pj);	
	outPoint->x = p.u;
	outPoint->y = p.v;
	//NSLog(@"projectPointFrom (%f,%f) to (%f, %f)", inPoint->x, inPoint->y, outPoint->x, outPoint->y);

- (NSPoint)rawCoordinatesForProjPoint:(NSPoint)projCoords
	NSPoint rawCoords = NSZeroPoint;
	projUV p;
	p.u = projCoords.x;
	p.v = projCoords.y;
	p = pj_inv(p, pj);
	rawCoords.x = p.u / DEG_TO_RAD;
	rawCoords.y = p.v / DEG_TO_RAD;
//	NSLog(@"in -rawCoordinatesForProjPoint:, input: (%f, %f), output: (%f, %f)",
//		  projCoords.x, projCoords.y, rawCoords.x, rawCoords.y);
	return rawCoords;

- (NSPoint)projCoordinatesForRawPoint:(NSPoint)rawCoords
	projUV p;
	NSPoint projCoords = NSZeroPoint;
	p.u = rawCoords.x * DEG_TO_RAD;
	p.v = rawCoords.y * DEG_TO_RAD;
	p = pj_fwd(p, pj);
	projCoords.x = p.u;
	projCoords.y = p.v;
//	NSLog(@"in -projCoordinatesForRawPoint:, input: (%f, %f), output: (%f, %f)",
//		  rawCoords.x, rawCoords.y, projCoords.x, projCoords.y);
	return projCoords;

You’ll want to copy and modify -becomePlatteCarre as appropriate, to expose other projections to your app.

Further comments

William Kyngesburye had these comments/corrections via the PROJ mailing list on maptools.org:

Some comments:

It’s not that I don’t build the PROJ framework with Xcode, but that I hadn’t updated the project files for download. I took a few minutes and did that. Note that it’s for PROJ 4.5 for now, as I haven’t looked at 4.6 yet because of the datum behaviour change. The project includes the patches I use to make the NAD file handling endian-agnostic so PROJ can be built universal.

Even so, remember that if you make adjustments to build it from the start to use @executable_path, the PROJ_LIB setting should still be left as is (or ignored), and the library initialized at runtime as you worked out.

The Copy Files + install_name_tool you worked out is pretty standard fare, so don’t knock it. Some may like to tweak things and build it all from scratch. Others may prefer to use as much binary source as possible. Both methods are completely valid.

Full paths to framework internals is preferable (other than the @executable_path part). That is, don’t depend on symlink shortcuts, but use the full “Versions/x.y/PROJ” path inside the framework. At least, this is the recommendation for installed frameworks, but could apply to bundled frameworks. I think it’s mostly a versioning safety thing.

The GeoMoose is Loose

The GeoMoose project yesterday announced their 1.0 release. GeoMoose is an AJAX interface for MapServer. It appears to be ideal for organizations that want to put GIS-like capabilities on the web, with end-user control of layer visibility, attributes, and view parameters.

I spent some time last night trying out the GeoMoose demos. It feels really fast. Zooming and panning are faster than on Google Maps. Image quality is pretty good. I told one of the developers that I think they waited too long to release their 1.0 version; the overall feel is very mature.

I haven’t looked into the back end, so I can’t say anything about ease of configuration or installation. GeoMoose worked for me on Firefox and on Safari 3.0, but not on Safari 2.0, so Mac OS X Tiger users will need to install the 3.0 beta (or wait for Leopard).

GeoMoose should get a look by anyone who wants to put large quantities of GIS data on the web. Any site where the end user is dynamically controlling the map layers or interacting with the data is a candidate for GeoMoose. It looks like a good peer of MapGuide Open Source but is a pure MapServer solution. The GeoMoose demo sites were substantially faster than similar sites I’ve seen for viewing property parcel data–but those slower sites were built with ESRI software.

Technorati Tags:

FOSS4G 2007

I just returned from the FOSS4G (Free and Open Source Software for Geospatial) 2007 conference in Victoria, BC. My main purpose was to make some face-to-face connections with people in the Mapserver project who I had only known over the net. The conference turned out to be much more than that though. There were some really wonderful projects represented that I had never heard of, and that I became an instant fan of. The conference gave me a great return on time and money.

I gave a talk Thursday morning on the sailwx.info ship tracker. It was fun to get some direct feedback, and I even managed to meet a few of the users, which is sometimes hard to do with a website.

My personal highlights from the week (aside from the great contacts I made):

  • OSSIMPlanet was visible all through the week, and was featured in several talks. OSSIMPlanet is just like Google Earth, except it’s open source, and you can use your own data from any OGC-compliant source, and you can synchronize multiple computers across a network (think instant display wall), and there are no license restrictions on your use of the product or the images, and you can use it without being connected to the Internet. I spent an entire Saturday afternoon playing with this great tool. It runs quite nicely on my MacBook Pro, but is also being used for very large cluster-based installations.
  • The GeoServer project gets my vote for best surprise of the week. This is a geodata server that does everything you might want. It delivers your geographic data as KML, GeoRSS, WMS, WFS, and many more formats. My favorite GeoServer demos revolved around Google Earth. GeoServer does on-the-fly switching between point and raster KML: if the client asks for a huge number of points, GeoServer delivers an image-based KML file, and when the client zooms in to a smaller number of points GeoServer switches over to point-based KML. The other great demo was a display (in Google Earth) of time-series KML. GeoServer is using open, published standards to do all this, so I expect you could switch to OSSIMPlanet instead of Google Earth if it strikes your fancy. If you are writing your own code to generate KML or GeoRSS, don’t. Switch to GeoServer instead.
  • MapGuide Open Source looks like a great tool for deploying true GIS functions over the web. I don’t have a need for this functionality at present, so didn’t dive in deeply, but I am keeping this one in my back pocket for when I do need to have remote users updating a central GIS over the network.
  • The Virtual Terrain Project is a visualization tool I wish I had had 10 or 15 years ago. Ben Discoe ran us through a hands-on workshop that had us all independent and productive in 90 minutes. My only dig is that the Mac version is still based on Fink, so the installation is very un Mac-like.
  • Attended a great workshop on using WMS with MapServer. This was one of those things that had been on my “to-do” list for some time, but that I never managed to make time to learn.

Google was a sponsor of the conference, and hosted a BoF session to solicit feedback for libkml, a Google project to produce a reference KML reader/writer. They were recruiting, and had several “summer of code” students demonstrating their results. I took the opportunity to wish the Google reps a happy 9th birthday on Thursday, but was met with blank stares.

I came back on the ferry Coho, with several other FOSS4G attendees. When my turn came to meet the US Customs inspector, he asked me why I had gone to Canada. I said “attended the FOSS4G conference”, and he replied “oh, the Free and Open Source…?” It’s pretty cool when even the customs agents know about open source software.

As an aside, this conference has finally pushed me over the edge into modern blogging (although I claim that my Cape Horn travelogue was a blog before there were blogs). I’ll also be updating an RSS feed on sailwx.info as that site continues to grow.

Now back to Mac development.

Technorati Tags:
, , ,