As Marx2 told me in the github pull request commented in the previous entry, the runalyze project is really nice. The previous weekend I was testing and uploading some of my running activities just to try it. And, in general, the impression is quite good. Obviously is much more complete than my little application RunnerUpWeb and works smoothly (actually the application is much more than what I needed and looked for).
At the same time I studied how the files were added (just in case I have the time to integrate it inside runnerup phone application). I tested with TCX files (the ones I usually use in my application and, therefore, I had easy access to them) and the process that runalyze uses is, at least, strange. When you want to upload any type of file the procedure that is followed is more or less the following:
When the upload button is clicked you can choose a file from your file system (a lot of formats are allowed, TCX among them).
The file is uploaded using a multipart/form-data post but nothing more, I mean, the file is uploaded to a directory inside the server but no real parsing is done. The file is just placed in the system (in a folder of the system more concretely).
After the file is uploaded another request to the server triggers the parsing (TCX parsing in my case). And the result of this operation is creating a huge HTML form with a lot of inputs (the real data obtained from the TCX file but exposed as a form). The HTML form is incrusted in the page as another tab in the import screen.
In that form the user can modify some inputs of the activity (some information that can be added or modified to the parsed data).
Finally the user submits that form and the activity is actually saved in the system (inside the ddbb).
At first I was thinking about replicating the same procedure with runnerup but it is a waste of time. I think now that the most proper way of integrating runnalize into a runnerup is just constructing the post data directly (all the needed input that the import process generates from the TCX file in the web application) and sending that to the runalyze. This idea means that the phone should generate a proper form data to post and only executes the last point of the procedure (point 5 in the previous explanation). Obviously that information should be generated reading the activity data stored in the phone database (exactly as it is done for generating a TCX or GPX file).
I am going to add here the java method that was tested to send the data (raw data copied from the browser) to the runalyze server.
public void submit() throws Exception {
URL url = new URL(url + "/call/call.Training.create.php?json=true");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setInstanceFollowRedirects(false);
conn.setDoOutput(true);
addHeadders(conn); // set the session cookie into the request
try (OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream())) {
StringBuilder sb = new StringBuilder()
.append("creator=").append("&")
.append("creator_details=").append("&")
.append("activity_id=1461954855").append("&")
.append("timezone_offset=120").append("&")
.append("arr_time=2|4|6|...").append("&")
.append("arr_lat=40.463737|40.463726|40.46372|...").append("&")
.append("arr_lon=-3.7070801|-3.7070918|-3.707106|...").append("&")
.append("arr_geohashes=").append("&")
.append("arr_alt=754|753|753|...").append("&")
.append("arr_alt_original=").append("&")
.append("arr_heart=78|80|80|...").append("&")
.append("arr_dist=0|0.00161|0.00309|...").append("&")
.append("arr_cadence=").append("&")
.append("arr_power=").append("&")
.append("arr_temperature=").append("&")
.append("arr_groundcontact=").append("&")
.append("arr_vertical_oscillation=").append("&")
.append("arr_groundcontact_balance=").append("&")
.append("pauses=[{\"time\":302,\"duration\":2,\"hr-startv\":156,\"hr-end\":156},{\"time\":570,\"duration\":2,\"hr-start\":0,\"hr-end\":0},{\"time\":848,\"duration\":2,\"hr-start\":0,\"hr-end\":0},{\"time\":1138,\"duration\":2,\"hr-start\":0,\"hr-end\":0},{\"time\":1306,\"duration\":4,\"hr-start\":0,\"hr-end\":0},{\"time\":1448,\"duration\":2,\"hr-start\":0,\"hr-end\":0},{\"time\":1722,\"duration\":2,\"hr-start\":0,\"hr-end\":0},{\"time\":1908,\"duration\":2,\"hr-start\":0,\"hr-end\":0},{\"time\":1912,\"duration\":2,\"hr-start\":0,\"hr-end\":0},{\"time\":2038,\"duration\":2,\"hr-start\":0,\"hr-end\":0}]").append("&")
.append("hrv=").append("&")
.append("fit_vdot_estimate=0.00").append("&")
.append("fit_recovery_time=0").append("&")
.append("fit_hrv_analysis=0").append("&")
.append("fit_training_effect=").append("&")
.append("fit_performance_condition=").append("&")
.append("elapsed_time=2288").append("&")
.append("elevation_calculated=0").append("&")
.append("groundcontact=0").append("&")
.append("vertical_oscillation=0").append("&")
.append("groundcontact_balance=0").append("&")
.append("vertical_ratio=0").append("&")
.append("stroke=").append("&")
.append("stroketype=").append("&")
.append("total_strokes=0").append("&")
.append("swolf=0").append("&")
.append("pool_length=0").append("&")
.append("weather_source=").append("&")
.append("is_night=0").append("&")
.append("distance-to-km-factor=1").append("&")
.append("sportid=1").append("&")
.append("typeid=1").append("&")
.append("is_race_sent=true").append("&")
.append("time_day=29.04.2016").append("&")
.append("time_daytime=18:34").append("&")
.append("s=37:46").append("&")
.append("kcal=554").append("&")
.append("pulse_avg=152").append("&")
.append("pulse_max=174").append("&")
.append("distance=7.851").append("&")
.append("elevation=0").append("&")
.append("pace=4:49").append("&")
.append("power=0").append("&")
.append("cadence=0").append("&")
.append("splits[km][]=1.006").append("&")
.append("splits[time][]=5:00").append("&")
.append("splits[active][]=1").append("&")
.append("splits[km][]=1.005").append("&")
.append("splits[time][]=4:30").append("&")
.append("splits[active][]=1").append("&")
.append("splits[km][]=1.004").append("&")
.append("splits[time][]=4:40").append("&")
.append("splits[active][]=1").append("&")
.append("splits[km][]=1.000").append("&")
.append("splits[time][]=4:52").append("&")
.append("splits[active][]=1").append("&")
.append("splits[km][]=1.004").append("&")
.append("splits[time][]=5:16").append("&")
.append("splits[active][]=1").append("&")
.append("splits[km][]=1.003").append("&")
.append("splits[time][]=4:36").append("&")
.append("splits[active][]=1").append("&")
.append("splits[km][]=1.004").append("&")
.append("splits[time][]=5:22").append("&")
.append("splits[active][]=1").append("&")
.append("splits[km][]=0.875").append("&")
.append("splits[time][]=3:50").append("&")
.append("splits[active][]=1").append("&")
.append("use_vdot=on").append("&")
.append("rpe=").append("&")
.append("comment=").append("&")
.append("partner=").append("&")
.append("route=").append("&")
.append("notes=").append("&")
.append("weatherid=1").append("&")
//.append("temperature=").append("&")
.append("wind_speed=").append("&")
.append("wind_deg=").append("&")
.append("humidity=").append("&")
.append("pressure=").append("&")
.append("tag_old=").append("&")
.append("equipment_old=").append("&")
.append("submit=submit");
writer.write(sb.toString());
writer.flush();
conn.connect();
String response = getResponse(conn.getInputStream());
if (conn.getResponseCode() != 200 || !response.contains("The activity has been successfully created.")) {
throw new IllegalStateException("Error code " + conn.getResponseCode());
}
}
}
The java class uses a technique to save the headers of a previous login (as runnerup is doing right now) and add them to the next submit. That login method is not shown in the entry. As you see the activity data is sent using a lot of inputs. Some of them are arrays of numbers separated by a pipe (all the data for samples: time, latitude, longitude, altitude, distance,...). Others are just some fixed data (as the day and time of the activity, the pace, the sport and type,...). For example the information about pauses is more complicated (it is a json with time, duration and heart-rate at the beginning and the end of each pause). One input line is commented out (temperature), just to check if you can omit optional fields. If you sent all that information a new activity is created in runalyze. So now the problem is obtaining all those inputs from the internal runnerup database for the activity to export. My main concern about the process is how much stable it will be. Will runalyze change the way activities are created? Is this form more or less stable? If someone involved with the project reads the entry please comment below or contact me directly.
In general now I am quite busy and I do not know if I will have the time to develop all the synchronizer stuff. I am just creating this entry to not forget what I have already thought and to encourage any of you to also start the task. The problem seems to be just reading from the database and filling the post data (but you know that it will never be so easy at the end). I will try to develop the synchronizer but I cannot say when I will do it. So, if you have the time, please go for it.
Regards!
Comments