Build a station finder, part 2

In part 1 of Building a station finder, we set up a map with its center and panning locked to the DC area. Now, in part 2, we’ll add markers to the map by parsing a GeoJSON file, extract the data points from the file, and create markers for each Metro station.

Adding markers to the map

Now that we’ve set up our map, let’s add markers for the stations. We will use this GeoJSON file as our data source. We obtained it by parsing the data at the WMATA Developer Portal and transforming it into the GeoJSON format.

Save the GeoJSON file to your computer. Add the file to your project with File → Add Files to “Station Finder”. Make sure that you check “Copy Items if Needed”.

Verify that you added the file to your project by checking the project navigator:

verify file

Loading GeoJSON

Create a new method in ViewController.m called -loadStations. This method will load the GeoJSON file from disk and parse the contents.

- (void)loadStations
{
    // Load the stations from the local geojson file
    NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"stations" ofType:@"geojson"];

    // Make sure we can load the geojson file
    if (![[NSFileManager defaultManager] fileExistsAtPath:jsonPath]) {
        NSLog(@"Error! Could not find stations.geojson file.");
        return;
    }

    NSData *data = [NSData dataWithContentsOfFile:jsonPath];
    NSError *error = nil;

    // Deserialize the JSON into an array of features that we can iterate over
    NSDictionary *jsonDict = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data
                                                                     options:0
                                                                       error:&error];
    NSArray *stationFeatures = jsonDict[@"features"];
}

Add a line to the end of your -viewDidLoad method to invoke the new method:

[self loadStations];

Set a breakpoint at the end of the -loadStations method. To set a breakpoint, click the breakpoint state icon to activate it:

Next, find the closing } for -loadStations and click the space to the left of it to add the breakpoint.

A breakpoint lets you pause the code so you can check on variables. Once you’ve set the breakpoint, build and run the app to make sure it works. Check that the debugger shows that the stationFeatures array has 91 objects:

Point features

Let’s take a second to get familiar with the feature objects in our stations file. Here’s an example of a feature:

{
    "type": "Feature",
    "geometry": {
        "type": "Point",
        "coordinates": [
            -76.9750541388,
            38.8410857803
        ]
    },
    "properties": {
        "description": "Southern Ave",
        "marker-symbol": "rail-metro",
        "title": "Southern Ave",
        "url": "http://www.wmata.com/rider_tools/pids/showpid.cfm?station_id=107",
        "lines": [
            "Green"
        ],
        "address": "1411 Southern Avenue, Temple Hills, MD 20748"
    }
}

We’re going to need to extract the latitude and longitude from the coordinates array. It’s important to note that GeoJSON coordinates are specified in longitude, latitude format. (This is because longitude is the x value and the coordinates are x,y).

We also need the properties object from the feature. The properties are a generic way to store data in key/value format for a given feature. There are no restrictions on the key names or values so long as they are valid JSON. In this GeoJSON file, the relevant information about the station, like name and address, is stored in the properties object.

Extracting the data

The features array in the GeoJSON file is already loading properly, but we aren’t doing anything with the individual feature objects yet.

Each element of the array is going to be an NSDictionary, so let’s iterate over them. Add this to the end of your -loadStations method:

...
NSArray *stationFeatures = jsonDict[@"features"];

for (NSDictionary *feature in stationFeatures)
{
    // We only support point features right now
    if ([feature[@"geometry"][@"type"] isEqualToString:@"Point"])
    {
        // Create a CLLocationCoordinate2D with the long, lat values
        CLLocationCoordinate2D coordinate = {
          .longitude = [feature[@"geometry"][@"coordinates"][0] floatValue],
          .latitude  = [feature[@"geometry"][@"coordinates"][1] floatValue]
        };

        NSDictionary *properties = feature[@"properties"];

        // Create an RMPointAnnotation with our new coordinate and use the
        // title from the properties
        RMPointAnnotation *stationAnnotation =
        	[RMPointAnnotation annotationWithMapView:_mapView
                                          coordinate:coordinate
                                            andTitle:properties[@"title"]];

		// Store the properties object so we can refer to it later
       stationAnnotation.userInfo = properties;

        [_mapView addAnnotation:stationAnnotation];
    }
}

We already know that jsonDict[@"features"] is an array in our GeoJSON, and for clarity we assign it to a NSArray called stationFeatures.

Each item in the stationFeatures array is an NSDictionary, so we created a loop to iterate over them. The first thing we do is extract the latitude and longitude values and create a CLLocationCoordinate2D struct from them. (Notice that it’s a C structure, not an Objective-C object, hence no * pointer dereference operator before the coordinate variable name.)

We’re going to refer to our properties object more than once, so we simplify the code by assigning it to a NSDictionary called properties.

We’re going to want to create markers on our map. We’ll accomplish this by creating RMPointAnnotation objects. If you’ve used Apple’s MKMapKit framework then this should feel familiar.

We instantiate a RMPointAnnotation called stationAnnotation with our map view instance variable, the coordinate structure we created, and the title value found in our properties dictionary.

RMAnnotation (the superclass of RMPointAnnotation) has an NSDictionary property called userInfo that we can use to assign generic key/value information to it. This is where we stick our properties data so that we can get it a little later.

If you build and run, you should see generic blue markers that you can tap to see the station name.

Screenshot

Parsing in a background thread

Our stations load pretty quickly, but it would be better practice to do our GeoJSON parsing and marker creation on a background thread. Thanks to Apple’s Grand Central Dispatch framework, this won’t be a difficult task.

We’re going to use the default dispatch queue (thread) and run our code inside of a dispatch_async call:

dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(backgroundQueue, ^(void)
{
    // Put the JSON parsing code in here
});

We’ll need to make sure that we add our annotation to the map on the main queue, since all UI work needs to be performed on the main thread.

dispatch_async(dispatch_get_main_queue(), ^(void)
{
    [_mapView addAnnotation:stationAnnotation];
});

When you’re finished, your -loadStations method should look like this:

- (void)loadStations
{
    // Load the stations from the local geojson file
    NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"stations" ofType:@"geojson"];

    // Make sure we can load the geojson file
    if (![[NSFileManager defaultManager] fileExistsAtPath:jsonPath]) {
        NSLog(@"Error! Could not find stations.geojson file.");
        return;
    }

    NSData *data = [NSData dataWithContentsOfFile:jsonPath];
    NSError *error = nil;

    // Deserialize the JSON into an array of features that we can iterate over
    NSDictionary *jsonDict = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data
                                                                     options:0
                                                                       error:&error];

    // Create a background queue to perform our marker creation on
    dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(backgroundQueue, ^(void)
    {
        NSArray *stationFeatures = jsonDict[@"features"];

        for (NSDictionary *feature in stationFeatures)
        {
            // We only support point features right now
            if ([feature[@"geometry"][@"type"] isEqualToString:@"Point"])
            {
                // Create a CLLocationCoordinate2D with the long, lat values
                CLLocationCoordinate2D coordinate = {
                .longitude = [feature[@"geometry"][@"coordinates"][0] floatValue],
                .latitude  = [feature[@"geometry"][@"coordinates"][1] floatValue]
                };

                NSDictionary *properties = feature[@"properties"];

                // Create an RMPointAnnotation with our new coordinate and use the
                // title from the properties
                RMPointAnnotation *stationAnnotation =
        	        [RMPointAnnotation annotationWithMapView:_mapView
                                                      coordinate:coordinate
                                                        andTitle:properties[@"title"]];

		        // Store the properties object so we can refer to it later
            	stationAnnotation.userInfo = properties;

               dispatch_async(dispatch_get_main_queue(), ^(void)
               {
                   [_mapView addAnnotation:stationAnnotation];
               });
            }
        }
	});
}

Build and run, and the app should load the markers as it did before.

Continue to part 3

This section was quick, but we built a very important piece of code to load a local GeoJSON file, parse it, extract Point features, and put markers on our map.

In part 3, we’ll customize our markers to show the line colors for a given station and provide a button to show that station’s realtime arrivals web page.

Additional questions? Ask our support team or learn more about How Mapbox Works.