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
					key:(unsigned)projKey
{
	[self setProjInitString:
		[NSString stringWithFormat:@"+init=%@/%@:%d", 
			[MGProjection projDataDirectoryPath],
			projFilename,
			projKey]];
}

- (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.

Advertisements