Trail Maker Icon

Trail Maker

Release Date: 2013

Language Used:

Objective-C

Software Used:

Xcode

Platforms:

iOS

About:

Trail Maker is an app I made to easily create a trail of points while walking around in the world. My dad likes to tap maple trees around our house, and told me one night that it would be nice to have an app to mark each tree. This is the inspiration for the app.

The app uses the GPS in the user's phone to get their location, and allows marking of points while they walk around. They can also customize this data, including changing each point name, longitude and latitude, and adding a picture for each point. The app also features a wide variety of export options, including PDF, Google Earth KML, a native file export, and a bulk image export.

Trail Maker utilizes Apple's Core Data framework for managing trails and related data, and uses the Core Location and Mapkit frameworks to get the user's position and to display it.

Trail Maker is by far my most popular app, recieving more downloads per day than all of my other apps combined. Downloads especially peak during the summer months, which I can assume is because of people getting outdoors more during the nice weather. As a result, I have tried to make it a priority when deciding to update my apps. It is also the app I have gotten the most feedback on, something I was not used to before publishing it. For example, none of my other apps have resulted in email communications from users before. Obviously I like hearing that people are enjoying the app, but I also like hearing that people want more from it. Knowing that people are enjoying your app enough to give their opinions on where the app should go is a very humbling feeling, and is part of the reason why I started making mobile apps.

Code Samples

Point Marking

This code marks a point on a trail and saves it to a Core Data context.
// marks a point in the current trail
- (void)markLocationWithName:(NSString *)name longitude:(NSNumber *)longitude latitude:(NSNumber *)latitude timestamp:(NSDate *)timestamp image:(UIImage *)image {
    NSManagedObjectContext *context = [self managedObjectContext];
    
    Location *newLocation = [NSEntityDescription insertNewObjectForEntityForName:@"Location" inManagedObjectContext:context];
    [newLocation setLongitude:longitude];
    [newLocation setLatitude:latitude];
    [newLocation setTimestamp:timestamp];
    [newLocation setName:name];
    [newLocation setImage:UIImageJPEGRepresentation(image, 1.0)];
    [newLocation setThumbnail:UIImageJPEGRepresentation([ConvenienceMethods squareImageForImage:image withWidth:400.0f], 1.0)];
    [self.trail addLocationsObject:newLocation];
    
    // attempt to save the updated trail
    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"can't save! %@ %@", error, [error localizedDescription]);
    }
    
    // refresh locationsArray
    locationsArray = [ConvenienceMethods timestampSortedArrayFromSet:self.trail.locations];
    
    // refresh UI elements with new info from location
    [self setUIElementsText:self.trail shouldUpdateTableView:YES];
    
    // scroll the tableView to the bottom
    if (pointsTableView.contentSize.height > pointsTableView.frame.size.height)
    {
        CGPoint offset = CGPointMake(0, pointsTableView.contentSize.height - pointsTableView.frame.size.height);
        [pointsTableView setContentOffset:offset animated:YES];
    }
    
    [self showAllPointsOnMapAndZoomToShowAll:NO animated:NO];
}

PDF Creation

This code is responsible for rendering a PDF version of a trail, using two page templates. The first page has a large map of the entire trail and two points. The second page is just a list of points. The method will render the first page and, if necessary, subsequent pages until there are no more points left to render.
+ (void)createPDFInBackground:(NSArray *)items {
    NSString *fileName = [items objectAtIndex:0];
    Trail *trail = [items objectAtIndex:1];
    UIImage *mapImage = [items objectAtIndex:2];
    MBProgressHUD *progressHUD = [items objectAtIndex:3];
    
    // increment when we draw the map, and then after we draw each image
    float HUDIncrement = 1.0 / trail.locations.count;
    
    // Create the PDF context using the default page size of 612 x 792.
    UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
    
    CGRect page = CGRectMake(0, 0, 612, 792);
    
    
    NSMutableArray *locationsArray = [ConvenienceMethods timestampSortedArrayFromSet:trail.locations];
    
    // if we have 2 or less Locations, just draw the first page
    // otherwise draw the first page, then draw additional pages as needed
    if (locationsArray.count <= 2) {
        // Mark the beginning of a new page.
        UIGraphicsBeginPDFPageWithInfo(page, nil);
        
        UIView *firstPage = [self viewForXibName:@"FirstPage"];
        
        // draw the trail name, distance, and number of points
        [self drawFirstPageTrailInfo:trail forView:firstPage];
        
        // draw the map image
        UIImageView *imageView = (UIImageView *)[firstPage viewWithTag:3];
        [self drawImage:mapImage inRect:imageView.frame];
        
        progressHUD.progress += HUDIncrement;
        
        // draw the points (and boxes)
        int index = 4;
        while (locationsArray.count != 0) {
            [self drawLocation:[locationsArray objectAtIndex:0] startingIndex:index forView:firstPage];
            [locationsArray removeObjectAtIndex:0];
            index += 8;
            progressHUD.progress += HUDIncrement;
        }
    } else {
        // DRAW THE FIRST PAGE
        
        // Mark the beginning of a new page.
        UIGraphicsBeginPDFPageWithInfo(page, nil);
        
        UIView *firstPage = [self viewForXibName:@"FirstPage"];
        
        // draw the trail name, distance, and number of points
        [self drawFirstPageTrailInfo:trail forView:firstPage];
        
        // draw the map image
        UIImageView *imageView = (UIImageView *)[firstPage viewWithTag:3];
        [self drawImage:mapImage inRect:imageView.frame];
        
        progressHUD.progress += HUDIncrement;
        
        // draw the points (and boxes)
        int index = 4;
        while (locationsArray.count != trail.locations.count - 2) {
            [self drawLocation:[locationsArray objectAtIndex:0] startingIndex:index forView:firstPage];
            [locationsArray removeObjectAtIndex:0];
            index += 8;
            progressHUD.progress += HUDIncrement;
        }
        
        // DRAW ADDITIONAL PAGES
        
        // determine number of pages to draw (6 Locations per page)
        int initialNumberOfPages = locationsArray.count;
        int maxLocationsPerPage = 6;
        int numberOfPagesNeeded = 0;
        while (initialNumberOfPages > 0) {
            numberOfPagesNeeded += 1;
            initialNumberOfPages -= maxLocationsPerPage;
        }
        
        // run through each page
        for (int i = 0; i < numberOfPagesNeeded; i++) {
            // Mark the beginning of a new page.
            UIGraphicsBeginPDFPageWithInfo(page, nil);
            
            UIView *secondPage = [self viewForXibName:@"SecondPage"];
            
            // the number of Locations to draw for this page
            int locationsToDraw = 0;
            if (locationsArray.count >= 6) {
                locationsToDraw = 6;
            } else {
                locationsToDraw = locationsArray.count;
            }
            
            // the view tag index
            int index = 0;
            
            // draw each Location for this page
            while (locationsToDraw != 0) {
                [self drawLocation:[locationsArray objectAtIndex:0] startingIndex:index forView:secondPage];
                [locationsArray removeObjectAtIndex:0];
                index += 8;
                locationsToDraw -= 1;
                progressHUD.progress += HUDIncrement;
            }
            
            // reset the view tag index
            index = 0;
        }
    }
    
    
    // Close the PDF context and write the contents out.
    UIGraphicsEndPDFContext();
    
    // set the HUD mode to the spinning progress indicator, since we can't know exactly when the writing will be done
    progressHUD.mode = MBProgressHUDModeIndeterminate;
    
    // now that the file processing is done, post the notification to show the share options for the current file
    // picked up by DetailViewController
    [[NSNotificationCenter defaultCenter] postNotificationName:@"shareResourceAfterDoneProcessing" object:nil];

}

Google Earth KML Creation

This code creates a Google Earth KML version of a trail. This KML document can then be viewed in an application like Google Earth.
+ (void)createGoogleEarthFile:(NSString *)fileName withTrail:(Trail *)trail {
    NSArray *locations = [ConvenienceMethods timestampSortedArrayFromSet:trail.locations];
    KMLRoot *root = [[KMLRoot alloc] init];
    
    // final KML document
    KMLDocument *doc = [[KMLDocument alloc] init];
    root.feature = doc;
    
    // single line for the entire trail
    KMLLineString *line = [[KMLLineString alloc] init];
    
    // iterate through each point on the trail, add each to the main line for the trail
    for (int i = 0; i < locations.count; i++) {
        Location *currentLocation = [locations objectAtIndex:i];
        
        KMLPlacemark *placemark = [[KMLPlacemark alloc] init];
        placemark.name = currentLocation.name;
        [doc addFeature:placemark];
        
        KMLPoint *point = [[KMLPoint alloc] init];
        placemark.geometry = point;
        
        KMLCoordinate *coordinate = [[KMLCoordinate alloc] init];
        coordinate.latitude = currentLocation.latitude.doubleValue;
        coordinate.longitude = currentLocation.longitude.doubleValue;
        point.coordinate = coordinate;
        
        [line addCoordinate:coordinate];
    }
    
    // store the line in a KMLPlacemark, add that to the final KML document
    [line setAltitudeMode:KMLAltitudeModeRelativeToGround];
    KMLPlacemark *linePlacemark = [[KMLPlacemark alloc] init];
    [linePlacemark setGeometry:line];
    [doc addFeature:linePlacemark];
    
    // save the NSData version of the KML string to the given path
    NSData *data = [root.kml dataUsingEncoding:NSUTF8StringEncoding];
    [data writeToFile:fileName atomically:YES];
    
    // now that the file processing is done, post the notification to show the share options for the current file
    // picked up by DetailViewController
    [[NSNotificationCenter defaultCenter] postNotificationName:@"shareResourceAfterDoneProcessing" object:nil];
}