From ede4d218451fedefd5c33feb4ff11519e0e2a0d1 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 15 Jun 2021 18:25:03 +1200 Subject: [PATCH 1/2] Add route directly from strava --- app/event.php | 6 +++ app/strava.php | 84 ++++++++++++++++++++++++++++++++++++++++ css/rg2.css | 48 +++++++++++++++++++++++ html/infopanel.html | 4 ++ js/draw.js | 5 +++ js/gpstrack.js | 25 ++++++++++++ js/rg2getjson.js | 3 ++ js/rg2ui.js | 29 ++++++++++++++ js/utils.js | 94 +++++++++++++++++++++++++++++++++++++++++++++ rg2-config .txt | 6 ++- rg2api.php | 29 +++++++++----- 11 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 app/strava.php diff --git a/app/event.php b/app/event.php index ec74f1a9..774b3903 100644 --- a/app/event.php +++ b/app/event.php @@ -36,6 +36,12 @@ public static function getEvent($id) $all['results'] = result::getResultsForEvent($id); $all['routes'] = route::getRoutesForEvent($id); $all['API version'] = RG2VERSION; + if (defined('STRAVA_SECRET')){ + $all['strava'] = 'true'; + } else { + $all['strava'] = 'false'; + } + $output = json_encode($all); @file_put_contents(CACHE_DIRECTORY."all_".$id.".json", $output); } diff --git a/app/strava.php b/app/strava.php new file mode 100644 index 00000000..b15a7d8d --- /dev/null +++ b/app/strava.php @@ -0,0 +1,84 @@ +access_token; + + // just echo the token, don't get activities + //echo "{\"access_token\":" .$access_token. "}"; + + //get latest strava activities + $activities = strava::getActivities($access_token); + + echo $activities; + + } + + public static function getToken($code) + { + $url = 'https://www.strava.com/api/v3/oauth/token'; + $data = array('client_id' => STRAVA_CLIENT, 'client_secret' => STRAVA_SECRET, + 'code' => $code, 'grant_type' => 'authorization_code'); + + // use key 'http' even if you send the request to https://... + $options = array( + 'http' => array( + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => http_build_query($data) + ) + ); + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + if ($result === FALSE) { /* Handle error */ } + return $result; + } + + public static function getActivities($token) + { + $url = 'https://www.strava.com/api/v3/athlete/activities'; + + // use key 'http' even if you send the request to https://... + $options = array( + 'http' => array( + 'header' => "Authorization: Bearer " . $token, + 'method' => 'GET' + ) + ); + $context = stream_context_create($options); + $activities = file_get_contents($url, false, $context); + if ($activities === FALSE) { /* Handle error */ }; + + $result = "{\"access_token\":\"" .$token. "\", \"activities\":" .$activities."}"; + + return $result; + } + + public static function getActivityStream($data){ + + list($activity, $token) = explode('|', $data); + + $url = 'https://www.strava.com/api/v3/activities/'. $activity . '/streams?keys=latlng,time'; + + // use key 'http' even if you send the request to https://... + $options = array( + 'http' => array( + 'header' => "Authorization: Bearer " . $token, + 'method' => 'GET' + ) + ); + $context = stream_context_create($options); + $stream = file_get_contents($url, false, $context); + if ($stream === FALSE) { /* Handle error */ }; + + return $stream; + } + +} \ No newline at end of file diff --git a/css/rg2.css b/css/rg2.css index 4f19e393..f876b988 100644 --- a/css/rg2.css +++ b/css/rg2.css @@ -406,6 +406,54 @@ html, body { display: table-cell; } +.border{ + padding: 2px; +} + +.grid-container { + border: 1px solid silver; + border-radius: 3px; + display: grid; + grid-template-areas: + 'top top btn' + 'left right btn'; + grid-gap: 1px; + background-color: white; + padding: 2px; +} + +.grid-container > span { + background-color: white; + text-align: left; + padding: 1px 1px 1px 1px; + font-size: 12px; +} + +#act-title{ + text-align:left; + font-weight: bold; + grid-area:top; +} + +#act-type{ + grid-area:left; +} + +#act-date{ + grid-area:right; +} + +#act-btn{ + display: flex; + justify-content: center; + align-items: center; + border: 1px solid darkslategrey; + border-radius: 3px; + background-color: silver; + grid-area:btn; +} + + #rg2-manage-edit .ui-button { margin-top: 10px; margin-bottom: 10px; diff --git a/html/infopanel.html b/html/infopanel.html index 66996f1a..76f9c63f 100644 --- a/html/infopanel.html +++ b/html/infopanel.html @@ -75,6 +75,10 @@

Load GPS file (GPX or TCX)

+
+ +
+
diff --git a/js/draw.js b/js/draw.js index ff0f24b2..62da373f 100644 --- a/js/draw.js +++ b/js/draw.js @@ -28,6 +28,10 @@ this.gpstrack.uploadGPS(evt); }, + uploadStrava : function (evt) { + this.gpstrack.uploadStrava(evt); + }, + getControlXY : function () { return {x: this.controlx, y: this.controly}; }, @@ -341,6 +345,7 @@ $("#btn-three-seconds").button('enable'); // setting value to null allows you to open the same file again if needed $("#rg2-load-gps-file").val(null).button('enable'); + $("#btn-get-strava").button('enable'); rg2.redraw(false); }, diff --git a/js/gpstrack.js b/js/gpstrack.js index 12db5fd9..7ba54aa9 100644 --- a/js/gpstrack.js +++ b/js/gpstrack.js @@ -119,6 +119,31 @@ } } }, + + uploadStrava : function(json){ + for (i=0; i < json.data.length; i++){ + + const d = json.data[i]; + + if (d.type === 'latlng'){ + + for (l=0; l < d.data.length; l++){ + const lat = d.data[l][0]; + const lon = d.data[l][1]; + this.lat.push(lat); + this.lon.push(lon); + }; + + } else if (d.type === 'time'){ + + for (t=0; t < d.data.length; t++){ + const tm = d.data[t] + this.routeData.time.push(tm) + }; + } + }; + this.processGPSTrack(); + }, getStartOffset : function (timestring) { var secs; diff --git a/js/rg2getjson.js b/js/rg2getjson.js index 9b0c70e9..9c0f4ec0 100644 --- a/js/rg2getjson.js +++ b/js/rg2getjson.js @@ -127,6 +127,9 @@ rg2.courses.generateControlList(rg2.controls); $("#btn-toggle-controls").show(); $("#btn-toggle-names").show(); + if (json.data.strava ==='false'){ + $("#btn-get-strava").hide(); + } processResults(json); processGPSTracks(json); rg2.eventLoaded(); diff --git a/js/rg2ui.js b/js/rg2ui.js index 45975ccb..48af2e93 100644 --- a/js/rg2ui.js +++ b/js/rg2ui.js @@ -145,6 +145,7 @@ $("#btn-reset-drawing").button().button("disable").click(function () { rg2.drawing.resetDrawing(); }); + $("#btn-get-strava").button().button("disable"); $("#btn-save-gps-route").button().button("disable").click(function () { rg2.drawing.saveGPSRoute(); }); @@ -663,6 +664,34 @@ $("#rg2-load-gps-file").change(function (evt) { rg2.drawing.uploadGPS(evt); }); + + $("#btn-get-strava").click(function(){ + //open oauth popup. return list of latest activities. + rg2.utils.oauthpopup({ + path: 'https://www.strava.com/oauth/authorize?client_id=65468&response_type=code&redirect_uri='+window.location.protocol+'//'+window.location.hostname+'/rg2/rg2api.php&approval_prompt=force&scope=activity:read_all', + callback: function(activities){ + + //parse the returned json + var activ = JSON.parse(activities); + var act = activ.activities; + var token = activ.access_token; + + //reset div to empty + document.getElementById('strava-activities').innerHTML = ''; + //disable load button + $("#btn-get-strava").button("disable"); + + //add current activities to div so user can select one + //limit to 10 + for (var i=0; i<10; i++){ + var a = act[i]; + var activityId = a.id.toString() + rg2.utils.addStravaActivitySelector('strava-activities', activityId, a.name, token, a.type, a.start_date_local); + } + } + }); + }); + }, configureUI: function () { diff --git a/js/utils.js b/js/utils.js index 5e3f252e..0100d2f0 100644 --- a/js/utils.js +++ b/js/utils.js @@ -2,6 +2,100 @@ /*global rg2Config:false */ (function () { var utils = { + + oauthpopup : function (options){ + // Open another window for oauth authorisation. Pass token back to main window when successful. + options.windowName = options.windowName || 'ConnectWithOAuth'; // should not include space for IE + options.windowOptions = options.windowOptions || 'location=0,status=0,width=800,height=400'; + options.callback = options.callback || function(){ window.location.reload(); }; + var that = this; + console.log(options.path); + that._oauthWindow = window.open(options.path, options.windowName, options.windowOptions); + that._oauthInterval = window.setInterval(function(){ + + var activities = ''; + + if (that._oauthWindow.document.body.innerHTML.startsWith('{"access_token":')){ + activities = that._oauthWindow.document.body.innerHTML; + that._oauthWindow.close(); + } + + if (that._oauthWindow.closed) { + window.clearInterval(that._oauthInterval); + options.callback(activities); + } + }, 1000); + }, + + addStravaActivitySelector: function (elementId, activityId, name, token, type, dt){ + + //Reformat string as date + dat = dt.substring(0,10)+ " "+ dt.substring(11,16) + + //create button elements + var border = document.createElement("div"); + border.className= 'border'; + + var act = document.createElement("div"); + act.className= 'grid-container'; + + var actTitle = document.createElement("span"); + actTitle.setAttribute("id", "act-title"); + actTitle.innerText = name; + act.appendChild(actTitle); + + var actType = document.createElement("span"); + actType.setAttribute("id", "act-type"); + actType.innerText = type; + act.appendChild(actType); + + var actDate = document.createElement("span"); + actDate.setAttribute("id", "act-date"); + actDate.innerText = dat; + act.appendChild(actDate); + + var btn = document.createElement("span"); + btn.setAttribute("id", "act-btn"); + btn.className = "act-btn"; + btn.innerText = 'Add'; + act.appendChild(btn); + + //request activity stream onclick and load to interface. + btn.onclick = function() { + btn.innerText = "Added"; + + //disable other buttons + var cusid_ele = document.getElementsByClassName('act-btn'); + for (var i = 0; i < cusid_ele.length; ++i) { + var item = cusid_ele[i]; + if (item.innerText === 'Add'){ + //hide other buttons. Not strictly necessary as clicking different add will override + //but probably more obvious to the user if we don't allow it? + item.style.visibility = 'hidden'; + } else { + //disable Added button + item.style.pointerEvents = 'none'; + } + } + + // get activity stream + $.getJSON(rg2Config.json_url, { + type : "stravaActivityStream", + id : activityId+'|'+token, + cache : false + }).done(function (json) { + rg2.drawing.uploadStrava(json); + }).fail(function (jqxhr, textStatus, error) { + /*jslint unparam:true*/ + reportJSONFail("Activities request failed: " + error); + }); + } + + border.appendChild(act); + document.getElementById(elementId).appendChild(border); + + }, + rotatePoint : function (x, y, angle) { // rotation matrix: see http://en.wikipedia.org/wiki/Rotation_matrix var pt = {}; diff --git a/rg2-config .txt b/rg2-config .txt index a3dc7e07..e1e48043 100644 --- a/rg2-config .txt +++ b/rg2-config .txt @@ -55,4 +55,8 @@ // // or //define('EPSG_CODE', "EPSG:12345|EPSG:67890"); - //define('EPSG_PARAMS', "+proj=x +ellps=WGS84 +datum=WGS84 +units=m +no_defs|+proj=y +ellps=WGS84 +datum=WGS84 +units=m +no_defs"); \ No newline at end of file + //define('EPSG_PARAMS', "+proj=x +ellps=WGS84 +datum=WGS84 +units=m +no_defs|+proj=y +ellps=WGS84 +datum=WGS84 +units=m +no_defs"); + + // If using strava, add the application oauth client and secret + //define('STRAVA_CLIENT', '') + //define('STRAVA_SECRET', '') \ No newline at end of file diff --git a/rg2api.php b/rg2api.php index f190766e..3141f14c 100644 --- a/rg2api.php +++ b/rg2api.php @@ -11,6 +11,7 @@ require(dirname(__FILE__) . '/app/splitsbrowser.php'); require(dirname(__FILE__) . '/app/user.php'); require(dirname(__FILE__) . '/app/utils.php'); + require(dirname(__FILE__) . '/app/strava.php'); require_once(dirname(__FILE__) . '/rg2-config.php'); @@ -55,18 +56,25 @@ } else { $id = 0; } + if (isset($_GET['code'])) { + $code = $_GET['code']; + } else { + $code = 'unknown'; + } - if ($_SERVER['REQUEST_METHOD'] == 'GET') { - handleGetRequest($type, $id); + if ($_SERVER['REQUEST_METHOD'] == 'GET' && $code == 'unknown') { + handleGetRequest($type, $id); + } elseif ($_SERVER['REQUEST_METHOD'] == 'GET' && $code !== 'unknown') { + strava::getStravaActivities($code); } elseif ($_SERVER['REQUEST_METHOD'] == 'POST') { - if ($type == 'uploadmapfile') { - map::uploadMapFile(); - } else { - handlePostRequest($type, $id); - } + if ($type == 'uploadmapfile') { + map::uploadMapFile(); + } else { + handlePostRequest($type, $id); + } } else { - header('HTTP/1.1 405 Method Not Allowed'); - header('Allow: GET, POST'); + header('HTTP/1.1 405 Method Not Allowed'); + header('Allow: GET, POST'); } function handlePostRequest($type, $eventid) @@ -186,6 +194,9 @@ function handleGetRequest($type, $id) event::fixResults($id); $output = json_encode("Results fixed for event ".$id); break; + case 'stravaActivityStream': + $output = strava::getActivityStream($id); + break; default: utils::rg2log("Get request not recognised: ".$type.", ".$id); $output = json_encode("Request not recognised."); From 9814c58cc08ac0e1f5caf8e518c88f7491f0931d Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 19 Jun 2021 13:37:33 +1200 Subject: [PATCH 2/2] Replace flexgrid with activity table - wasn't working in chrome --- css/rg2.css | 48 --------------------------------------------- html/infopanel.html | 2 +- js/utils.js | 27 +++++++++++++------------ 3 files changed, 15 insertions(+), 62 deletions(-) diff --git a/css/rg2.css b/css/rg2.css index f876b988..4f19e393 100644 --- a/css/rg2.css +++ b/css/rg2.css @@ -406,54 +406,6 @@ html, body { display: table-cell; } -.border{ - padding: 2px; -} - -.grid-container { - border: 1px solid silver; - border-radius: 3px; - display: grid; - grid-template-areas: - 'top top btn' - 'left right btn'; - grid-gap: 1px; - background-color: white; - padding: 2px; -} - -.grid-container > span { - background-color: white; - text-align: left; - padding: 1px 1px 1px 1px; - font-size: 12px; -} - -#act-title{ - text-align:left; - font-weight: bold; - grid-area:top; -} - -#act-type{ - grid-area:left; -} - -#act-date{ - grid-area:right; -} - -#act-btn{ - display: flex; - justify-content: center; - align-items: center; - border: 1px solid darkslategrey; - border-radius: 3px; - background-color: silver; - grid-area:btn; -} - - #rg2-manage-edit .ui-button { margin-top: 10px; margin-bottom: 10px; diff --git a/html/infopanel.html b/html/infopanel.html index 76f9c63f..1e25d82c 100644 --- a/html/infopanel.html +++ b/html/infopanel.html @@ -77,7 +77,7 @@

Load GPS file (GPX or TCX)

-
+
diff --git a/js/utils.js b/js/utils.js index 0100d2f0..39d0fde4 100644 --- a/js/utils.js +++ b/js/utils.js @@ -33,32 +33,36 @@ dat = dt.substring(0,10)+ " "+ dt.substring(11,16) //create button elements - var border = document.createElement("div"); - border.className= 'border'; + tr = document.getElementById(elementId).insertRow(); - var act = document.createElement("div"); - act.className= 'grid-container'; + var td = tr.insertCell(); var actTitle = document.createElement("span"); actTitle.setAttribute("id", "act-title"); actTitle.innerText = name; - act.appendChild(actTitle); + td.appendChild(actTitle); + + var td = tr.insertCell(); var actType = document.createElement("span"); actType.setAttribute("id", "act-type"); actType.innerText = type; - act.appendChild(actType); + td.appendChild(actType); + + var td = tr.insertCell(); var actDate = document.createElement("span"); actDate.setAttribute("id", "act-date"); actDate.innerText = dat; - act.appendChild(actDate); + td.appendChild(actDate); + + var td = tr.insertCell(); - var btn = document.createElement("span"); + var btn = document.createElement("button"); btn.setAttribute("id", "act-btn"); btn.className = "act-btn"; btn.innerText = 'Add'; - act.appendChild(btn); + td.appendChild(btn); //request activity stream onclick and load to interface. btn.onclick = function() { @@ -89,10 +93,7 @@ /*jslint unparam:true*/ reportJSONFail("Activities request failed: " + error); }); - } - - border.appendChild(act); - document.getElementById(elementId).appendChild(border); + } },