Wednesday, February 8, 2012

implementing StravaToCsv in Java

In an earlier post I wrote how I put together a "Strava_to_CSV" code using Perl in maybe half an hour. It was pretty easy once I'd installed the Perl JSON libary.

But my goal here is Java, not Perl, so I decided to try to write a similar code in Java. Well, I did it, but it took a lot longer than a half an hour (maybe 5 hours total) and it's a lot longer.

Of course, I know Perl fairly well, while Java I'm trying to learn, so the time is an unfair comparison. Still, in the Java there's stuff to look after.

I won't share the full code here, but will summarize the key points.

First, to get the data from Strava, I once again used their "streams" method from version 1 of the API:


System.err.println("processing Strava activity # " + stravaToCsv.activities[n]);
String urlString = "http://app.strava.com/api/v1/streams/" + stravaToCsv.activities[n];
System.err.println("accessing ride data via URL = " + urlString);
java.net.URL url = null;
try {
  url = new java.net.URL( urlString );
}
catch ( java.net.MalformedURLException exception ) {
  System.err.println( "URL is malformed : $url" );
  System.exit(1);
}
// get the data for the activity
s = GetUrl.getUrl(url);
Here "GetUrl" is a class I wrote based on an example I found on-line:

import java.io.*;
import java.net.*;

// reference for this class:
// http://www.devdaily.com/java/edu/pj/pj010011

class GetUrl {
  public static String getUrl(URL url) {
    InputStream inputStream = null;
    String buffer = null;
    StringBuilder sb = new StringBuilder();

    try {
      inputStream = url.openStream();

      // Convert the InputStream to a buffered DataInputStream.
      BufferedReader d = new BufferedReader(new InputStreamReader(inputStream));


      while ((buffer = d.readLine()) != null) {
        sb.append(buffer);
      }

    } catch (MalformedURLException exception) {

      System.err.println("ERROR malformed URL: " + url.toString());
      exception.printStackTrace();
      System.exit(1);

    } catch (IOException exception) {

      System.err.println("ERROR: I/O exception while reading URL: " + url.toString());
      exception.printStackTrace();
      System.exit(1);

    } finally {
      try {
        inputStream.close();
      } catch (IOException exception) {
        System.err.println("ERROR: I/O exception while closing URL: " + url.toString());
        exception.printStackTrace();
        System.exit(1);
      }
    }
    return sb.toString();
  }
}

So these give me the JSON data. In the Perl script, I cheated and used "wget" which is a separate code (on Apple OS/X, it would have been "curl"). I could have done something similar with Java, but wanted to stick within Java if possible.

The next was then to decode the JSON data. For that I needed a class in which to store it. Here's what I came up with:

class StravaActivityData {
    public String id;
    public double[][] latlng;
    public double[] time;
    public double[] distance;
    public double[] altitude;
    public double[] altitude_original;
    public double[] heartrate;
    public double[] cadence;
    public double[] watts_calc;
    public double[] watts;

    public StravaActivityData() {
    }

    int numPoints() {
        return time.length;
    }
}
The main point is I've defined arrays for all the data types I anticipate might be found in a Strava activity. This is bad, because if Strava changes the fields, I'd need to revise my code. On the other hand, the Perl code had special handling for "latlng" (whose elements are length-two arrays) but it assumed only that other fields would be scalar arrays. So with the Perl code, if Strava were to add fields (like "temperature" or "L-R balance", for example) then the Perl code would automatically pick these up. This class is fairly general, and I anticipate using it in my main project. I didn't want to bog it down with functionality I would use only for the CSV generator. So I then built a class on top of this one specific to the CSV project. Here's that one:

class StravaActivityDataCsv extends StravaActivityData {
    public void printHeader() {
       System.out.println("id,n,lat,lng,time,distance,altitude,altitude_original,cadence,heartrate,watts_calc,watts");
    }
    public void printData() {
      for (int n = 0; n < numPoints(); n ++) {
         System.out.printf("%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
            (id == null)                ? "" : id,
            n,
            (latlng == null)            ? "" : latlng[n][0],
            (latlng == null)            ? "" : latlng[n][1],
            time[n],
            (distance == null)          ? "" : distance[n],
            (altitude == null)          ? "" : altitude[n],
            (altitude_original == null) ? "" : altitude_original[n],
            (cadence == null)           ? "" : cadence[n],
            (heartrate == null)         ? "" : heartrate[n],
            (watts_calc == null)        ? "" : watts_calc[n],
            (watts == null)             ? "" : watts[n]
          );
                               
        }
    }
    public void printCsv() {
        printHeader();
        printData();
    }

}
It extends the prior class with a methods to print a comma-delimited list with available fields. Note fields might be missing, other than time, and so it checks to see if arrays are null before trying to extract components from those arrays. This is a bit cumbersome, but it does the job. I don't like that the list of field is fixed, that it doesn't just use whatever it finds in the JSON stream, but ah, well. So all I needed to do was to store the JSON stream downloaded with the Strava API in that class. But I needed to decode the JSON. I checked on various libraries to do this and saw a recommended for Google's GSON library. I figured if Google use it it must be good, and it did in fact work fairly well once I'd figured out the basics. First, I defined my GSON object:

import com.google.gson.*;
Gson gson = new Gson();
Then decode the JSON, and print the CSV stream:

 // process the string with GSON
 StravaActivityDataCsv stravaActivityDataCsv = gson.fromJson(s, StravaActivityDataCsv.class);
 stravaActivityDataCsv.id = stravaToCsv.activities[n];

 // print out the data
 stravaActivityDataCsv.printCsv();
Okay, so this is just a fragment of the total code. The point here is progress has been made from a starting position of very little. My StravaToCsv was sort a test bed for doing some of the stuff I need to do in my goal app, and so it's encouraging I was able to make it work.

No comments: