Creating Maps in Cocoa using NSBezierPath

 Creating Maps in a Cocoa App using NSBezierPath

In this tutorial we will create the USA Map using NSBezierPath.

The final output of the tutorial is as follows

NSBezierPath-Popover-Map1

NSBezierPath Map View

Please note that the map coordinates used in this tutorial is not 100% accurate.

Following are the steps to create the Map View application.

  1. Create a new Cocoa project and name it as USAMap
  2. Modify the NSWindow of the MainMenu.xib and add a view and specify the CustomClass as MapView (We will write the MapView class in a short while)
  3. Add a slider in the window that will be used to zoom the Map.
  4. Add a Custom View, this will be used as the information popover (The information is purely upto the implementer, here I am using a NSPopOver to display the custom view showing State Names and some descriptive text for each State.
  5. Include/Add the attached state border coordinate path file to your project.
  6. usmap.json.zip (Click on the link to download the json file having the USA State map border coordinates)
  7. Note: In this sample we have not used a ViewController or WindowController, (not a good design practice), The entire load is put of the NSView instance to parse and load the Map Coordinates from a JSON file. If you intend to use this in your app, move the file reading into a NSWindowController or NSViewController subclass and use the NSView instance only for rendering and drawing purpose.
  8. We have loaded the path coordinates JSON into a path array of actual NSBezierPath and are iterating through the path array inside the View drawRect method to do the actual drawing

 

MapView-XIB

//
//  MapView.h
//  KSWorldMap
//
//  Created by Debasis Das on 5/6/14.
//  Copyright (c) 2014 Debasis Das. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface MapView : NSView
{

}
@property (assign) IBOutlet NSView *tooltipView; //This is the view that will contain the text fields/labels to be displayed in the Popover
@property (assign) IBOutlet NSTextField *primaryLabelTextField;
@property (assign) IBOutlet NSTextField *secondLabelTextField;
@property (assign) IBOutlet NSTextView  *detailsTextView;
@property (nonatomic,retain) NSArray *dataArray; //The container for holding the data loaded from the JSON File.
@property (nonatomic,retain) NSMutableArray *pathArray; //The actual NSBezierPath placeholder. drawRect method will iterate through this array and do the drawing in the MapView
@property (nonatomic,retain) NSPopover *infoPopOver; //Popover to display information of the clicked state/area
@end
//
//  MapView.m
//  KSWorldMap
//
//  Created by Debasis Das on 5/6/14.
//  Copyright (c) 2014 Debasis Das. All rights reserved.
//

#import "MapView.h"

@implementation MapView
static float scale=1.0;
@synthesize dataArray;
@synthesize pathArray;
@synthesize tooltipView;
@synthesize primaryLabelTextField;
@synthesize secondLabelTextField;
@synthesize detailsTextView;
@synthesize infoPopOver;

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self recreatePaths]; 
    }
    return self;
}

//Method to load the dataArray from the JSON Path File and then convert the same into NSBezierPath and store in the pathArray
-(void)recreatePaths
{
    NSError *anError = nil;
    NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"usmap" ofType:@"json"]];
    dataArray =  [[NSArray alloc] initWithArray:[NSJSONSerialization JSONObjectWithData:data /*self.responseData*/ options:NSJSONReadingMutableContainers error:&anError]];
    NSArray *borderPathArray;
    [self setPathArray:nil];
    pathArray = [[NSMutableArray alloc] init];
    for (NSDictionary *dict in dataArray)
    {
        NSBezierPath *hPath = [NSBezierPath bezierPath];
        [hPath setLineWidth: 1.5];
        borderPathArray = [dict objectForKey:@"borders"];
        [hPath moveToPoint:NSMakePoint([[[borderPathArray objectAtIndex:0] objectAtIndex:0] floatValue]*scale, [[[borderPathArray objectAtIndex:0] objectAtIndex:1] floatValue]*scale)];
        for (int i=1; i<[borderPathArray count];i++)
        {
            [hPath lineToPoint:NSMakePoint([[[borderPathArray objectAtIndex:i] objectAtIndex:0] floatValue]*scale, [[[borderPathArray objectAtIndex:i] objectAtIndex:1] floatValue]*scale)];
        }
        [pathArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:hPath,@"path",[dict objectForKey:@"name"],@"name",[dict objectForKey:@"color"],@"color", nil]];
    }

}

//The popover is initialized in the awakeFromNib method
-(void)awakeFromNib
{
    if(self.infoPopOver == nil)
    {
        NSPopover *popover = [[NSPopover alloc] init];
        [self setInfoPopOver:popover];
        [self.infoPopOver setBehavior:NSPopoverBehaviorTransient];
        NSViewController *vC = [[NSViewController alloc] init];
        vC.view = tooltipView;
        [[vC view] setFrame:[tooltipView frame]];
        [self.infoPopOver setContentViewController:vC];
        [self.detailsTextView setTextColor:[NSColor whiteColor]];
    }
}
- (BOOL)isFlipped
{
    return YES; 
}

//The resizeScale method is mapped to the slider on the screen to zoom the MapView. it simply modifies the path coordinates by multiplying the scale value from the slider
-(IBAction)resizeScale:(id)sender
{
    scale = [sender floatValue];
    [self recreatePaths];
    [self setNeedsDisplay:YES];
}

//Display the Information Popover on clicking inside an area. This method iterates through the path array and finds the path that contains the Clicked Point. 
- (void)mouseDown:(NSEvent *)theEvent
{
    NSPoint p = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    for (int i=0; i<pathArray.count; i++)
    {
        if ([[[pathArray objectAtIndex:i] objectForKey:@"path"] containsPoint:p])
        {
            [self.primaryLabelTextField setStringValue:[[pathArray objectAtIndex:i] objectForKey:@"name"]];
            [self.secondLabelTextField setStringValue:[NSString stringWithFormat:@"%@ Secondary Info",[[pathArray objectAtIndex:i] objectForKey:@"name"]]];
            [self.detailsTextView setString:[NSString stringWithFormat:@"%@ Detailed Information.. \r This is a long text about %@",[[pathArray objectAtIndex:i] objectForKey:@"name"],[[pathArray objectAtIndex:i] objectForKey:@"name"]]];
            
            [self.infoPopOver showRelativeToRect:NSMakeRect(p.x, p.y, 10, 10) ofView:self preferredEdge:CGRectMaxXEdge];
        }
    }
}

//The actual method that takes the load of doing the drawing/plotting of the map.
- (void)drawRect:(NSRect)dirtyRect
{
    [super drawRect:dirtyRect];
    [NSGraphicsContext saveGraphicsState];
    [[NSColor blackColor] setFill];
    [[NSBezierPath bezierPathWithRoundedRect:[self bounds] xRadius:1.0 yRadius:1.0] fill];
    [NSGraphicsContext restoreGraphicsState];
    for (int i =0; i<pathArray.count; i++)
    {
        [[NSColor blackColor] set];
        [[[pathArray objectAtIndex:i] objectForKey:@"path"] stroke];
        if([[[pathArray objectAtIndex:i] objectForKey:@"color"] isEqualToString:@"red"])
        {
            [[NSColor redColor] set];
        }
        else if([[[pathArray objectAtIndex:i] objectForKey:@"color"] isEqualToString:@"blue"])
        {
            [[NSColor blueColor] set];
        }
        else
        {
            [[NSColor lightGrayColor] set];
        }
        [[[pathArray objectAtIndex:i] objectForKey:@"path"] fill];
    }
}
@end

 

Build and Run

NSBezierPath-Map1

MapView using NSBezierPath

NSBezierPath-Popover-Map1

Additional Information as a Popover on Click inside an Area/State or a path

NSBezierPath-ZoomedView-Map1

Zoomed Map View

Created By: Debasis Das

Posted in Cocoa, Objective C Tagged with: , ,

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Recent Posts


Hit Counter provided by technology news