Let'sRun jest serwisem internetowym dla biegaczy, zintegrowanym z aplikacją dla systemu Android, dokonującą pomiaru statystyk biegu. Pomysł na projekt zrodził się z analizy aplikacji dostępnych w Android Market. Obecnie istniejące tego typu aplikacje, są napisane w języku angielskim, co znacznie utrudnia korzystanie użytkownikom. Co więcej, po sprawdzeniu okazało się, że żadna z nich nie oferuje możliwości wysyłania i przechowywania statystyk, dzięki którym biegacze mogliby analizować swoje postępy w treningu. Obecnie nie istnieje żaden serwis społecznościowy, skupiający biegające osoby, który udostępniałby tego typu funkcjonalność.
Jako że Let'sRun jest projektem unikatowym na rynku oraz nastawionym na młode, dbające o swój styl życia osoby, można śmiało przypuszczać że doskonale wypełni lukę istniejącą na rynku i wzbudzi zainteresowanie firm związanych z tematyką sportową.
Obecnie praktycznie nie istnieją produkty które udostępniałyby wszystkie funkcjonalności aplikacji projektu Let'sRun. Istnieją natomiast aplikacje takie jak Run.GPS Trainer Lite czy Running Tracker, które udostępniają zbliżone funkcjonalności. Każda z nich umożliwia odczyt podstawowych parametrów biegu, taki jak: aktualna prędkość, przebiegnięty dystans, czas biegu. Nie istnieje natomiast internetowy serwis społecznościowy, który umożliwiałby przechowywanie wyżej wymienionych statystyk. Let'sRun jest produktem niewątpliwie innowacyjnym. Po pierwsze, udostępnia użytkownikom aplikację do monitorowania parametrów biegu w czasie rzeczywistym i w języku polskim, co jest ewenementem na rynku. Po drugie, jego częścią jest serwis społecznościowy, który umożliwia przechowywanie statystyk zebranych przez aplikację mobilną i monitorowanie postępów w treningu, oraz porównywanie swoich wyników z innymi biegaczami. Obydwa przedstawione rozwiązania celują w grupę młodych, nowoczesnych i dobrze sytuowanych młodych ludzi, dając im produkt który jest przez rynek pożądany.
W części projektu Let’s Run związanej z istnieniem serwisu internetowego, przewiduje się trzy typy użytkowników: administrator, użytkownik niezarejestrowany i zarejestrowany. Administrator będzie posiadał pełne prawa zarządzania serwisem, zarówno od strony merytorycznej jak i technicznej. Będzie to możliwe dzięki dostępowi tak do bazy danych jak i do treści poszczególnych podstron. Aby ułatwić edycję treści pojawiających się na stronie, użytkownik będzie korzystał z systemu zarządzania treścią Drupal w wersji 6.20 (Drupal CMS). Użytkownik niezarejestrowany będzie miał ograniczony dostęp do serwisu, a więc poza stronami informacyjnymi, będzie mógł przeglądać jedynie rankingi ogólne, jak również otrzyma możliwość założenia konta. Użytkownik zarejestrowany będzie mógł przeglądać strony dostępne dla użytkownika niezarejestrowanego, jak również (po zalogowaniu) indywidualne statystyki każdego pojedynczego biegu, a także podsumowania i statystyki dla wybranego okresu czasu.
Aplikacja mobilna będzie korzystała z bazy danych typu SQLitle i SDK Android w wersji 1.6 by zapewnić wsteczną kompatybilność. Dla łatwego korzystania z aplikacji zostanie wykonany interfejs użytkownika. Podstawowymi operacjami wykonywanymi na bazie będą select, insert oraz remove. Aplikacja będzie również synchronizować bazę z bazą serwisu internetowego za pomocą funkcji httpresponse(). Wszystkie funkcje obsługiwane przez aplikację będą tworzone w oparciu o SDK Android 1.6.
CREATE TABLE users ( userid INT UNSIGNED NOT NULL AUTO_INCREMENT, login CHAR(50) NOT NULL, password CHAR(50) NOT NULL, email CHAR(50) NOT NULL, confirmed CHAR(32) NOT NULL, distance INT NOT NULL, max_distance INT NOT NULL, time INT NOT NULL, max_time INT NOT NULL, kcal INT NOT NULL, max_kcal INT NOT NULL, avg_speed INT NOT NULL, max_speed INT NOT NULL, PRIMARY KEY (userid) ) ENGINE=INNODB; CREATE TABLE tracks ( trackid INT UNSIGNED NOT NULL AUTO_INCREMENT, userid INT UNSIGNED NOT NULL, distance INT NOT NULL, time INT NOT NULL, kcal INT NOT NULL, avg_speed FLOAT(6,2) NOT NULL, maximum_speed INT NOT NULL, date_time DATETIME NOT NULL, PRIMARY KEY (trackid), FOREIGN KEY (userid) REFERENCES users(userid) ON DELETE CASCADE ) ENGINE=INNODB;
Znaczenie pól w bazie po stronie aplikacji mobilnej identyczne jak w przypadku aplikacji webowej.
Baza danych spełnia 1NF ponieważ każda składowa w każdej kropce jest atomowa i nie da się jej podzielić.
Baza danych spełnia 2NF ponieważ spełnia 1NF oraz każdy element jest zależnie funkcyjny od kluczy poszczególnych tabeli.
Baza danych spełnie 3NF ponieważ spełnia 2NF oraz każdy atrybut jest bezpośrednio zależny od klucza głównego w poszczególnych tabelach.
Poniższe zapytania wykorzystują zmienne PHP, przechowujące odpowiednie wartości:
Włożenie do bazy nowej trasy:
INSERT INTO tracks (userid,distance,time,kcal,maximum_speed,avg_speed,date_time) VALUES ('.$userid.','.$distance.','.$time.','.$kcal.','.$maximum_speed.','.$avg_speed.',"'.$date_time.'")
Update tabeli users przechowującej wartości maksymalne dla użytkownika - niemożliwe było zastosowanie triggera z racji ograniczeń nałożonych na wykorzystywany hosting:
UPDATE users SET distance='.$udistance.', max_distance='.$umax_distance.', time='.$utime.', max_time='.$umax_time.', kcal='.$ukcal.', max_kcal='.$umax_kcal.', avg_speed='.$uavg_speed.', max_speed='.$umax_speed.' WHERE userid='.$userid
Dodanie nowego użytkownika (wartość md5 pozwala na identyfikację potwierdzonego użytkownika):
INSERT INTO users (login,password,email,confirmed,distance,max_distance,time,max_time,kcal,max_kcal,avg_speed,max_speed) VALUES ("'.$login.'","'.$password.'","'.$email.'","'.$md5.'",0,0,0,0,0,0,0,0)
Autoryzacja (potwierdzenie użytkownika):
UPDATE users SET confirmed="1" WHERE confirmed="'.$confirm.'"
Zapytania wykorzystywane do tworzenia rankingów (w zależności od typu rankingu oprócz loginu, pobierana była odpowiednia wartość a wiec distance, max_distance itd.):
SELECT login,distance FROM users ORDER BY distance DESC
Statystyki pojedynczego biegu dla zalogowanego użytkownika w oparciu o zmienną sesji:
SELECT date_time, distance, time, maximum_speed, kcal FROM tracks WHERE userid='.$_SESSION['luserid'].' AND trackid="'.$trackid.'"
Wybór tras dla interwałów czasowych, kolejno: ostatni tydzień, dwa ostatnie tygodnie, ostatni miesiąc, przedział czasowy wprowadzony przez użytkownika:
SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 7 DAY) AND NOW() SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 14 DAY) AND NOW() SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 30 DAY) AND NOW() SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN "'.$start.'" AND "'.$end.'"
oraz stworzenie statystyk dla wybranego okresu:
SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 7 DAY) AND NOW() SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 14 DAY) AND NOW() SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 30 DAY) AND NOW() SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN "'.$start.'" AND "'.$end.'"
Dla gromadzenia informacji o biegach użytkowników serwisu Let's Run, posłuży nam aplikacja na telefony posiadające system operacyjny Android w wersji od 1.6 w wzwyż (każda wyższa wersja zapewnia wsparcie dla wersji poprzednich, dzieje się tak dzięki polityce producenta systemu firmy Google). Wybór tej wersji Android SDK, został podyktowany tym iż jest ono zamkniętym projektem i w pełni funkcjonalny.
Nasz produkt jest aplikacją przeznaczoną do śledzenia statystyk biegów, które następnie zapisywane są w bazie SQLite aplikacji. Na żądanie użytkownika, po podaniu logina i hasła, wszystkie biegi są wysyłane na serwer aplikacji webowej. W tym momencie dane zapisywane są w bazie i przeliczane są nowe statystyki użytkownika, po zakończeniu tej operacji statystyki zostają wysłane na urządzenie mobilne.
W celu spełnienia tych zadań zostały stworzone następujące mechanizmy:
private static final String DATABASE_NAME = "letsrun.db"; private static final int DATABASE_VERSION = 40; public static final String TABLE_USERS = "users"; public static final String TABLE_TRACKS = "tracks"; public MyOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_USERS + " (userid INTEGER PRIMARY KEY AUTOINCREMENT," + "login CHAR(50) NOT NULL," + "password CHAR(50) NOT NULL," + "email CHAR(50) NOT NULL,"+ "confirmed CHAR(32) ," + "distance INTEGER DEFAULT 0," + "max_distance INTEGER DEFAULT 0," + "time INTEGER DEFAULT 0," + "max_time INTEGER DEFAULT 0," + "kcal INTEGER DEFAULT 0," + "max_kcal INTEGER DEFAULT 0," + "avg_speed INTEGER DEFAULT 0," + "max_speed INTEGER DEFAULT 0" + ");"); db.execSQL("CREATE TABLE " + TABLE_TRACKS + " (_id INTEGER PRIMARY KEY AUTOINCREMENT," + "userid INTEGER NOT NULL, " + "distance INTEGER NOT NULL," + "time INTEGER NOT NULL," + "kcal INTEGER NOT NULL," + "avg_speed INTEGER NOT NULL," + "maximum_speed INTEGER NOT NULL," + "date_time TEXT NOT NULL," + "synchronized INTEGER NOT NULL DEFAULT 0," + "FOREIGN KEY (userid) REFERENCES users(userid)" + ");"); ContentValues values = new ContentValues(); values.put("login", "user"); values.put("password", ""); values.put("email", ""); db.insert(TABLE_USERS, null, values); Cursor s = db.query("users", new String[]{"*"}, null, null, null, null, null); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS); db.execSQL("DROP TABLE IF EXISTS " + TABLE_TRACKS); onCreate(db); }
private Context context; private SQLiteDatabase database; private MyOpenHelper dbHelper; public DatabaseAdapter (Context context) { this.context = context; } public DatabaseAdapter open() throws SQLException { try{ dbHelper = new MyOpenHelper(context); database = dbHelper.getWritableDatabase(); } catch (SQLException s) { Log.d("", "XXX" + s.toString()); } return this; } public void close() { dbHelper.close(); } public void AddToUsers (String login, String password, String email, String confirmed, int distance, int maxdist, int time, int maxtime, int kcal, int maxkcal, int avgspeed, int maxspeed) { ContentValues values = new ContentValues(); values.put("login", login); values.put("password", password); values.put("email", email); values.put("confirmed", confirmed); values.put("distance", distance); values.put("max_distance", maxdist); values.put("time", time); values.put("max_time", maxtime); values.put("kcal", kcal); values.put("max_kcal", maxkcal); values.put("avg_speed", avgspeed); values.put("max_speed", maxspeed); database.insertOrThrow(MyOpenHelper.TABLE_USERS, null, values); } public void AddToTracks (int userid, int distance, int time, int kcal, int avgspeed, int maxspeed, String datetime, int synchro) { ContentValues values = new ContentValues(); values.put("userid", userid); values.put("distance", distance); values.put("time", time); values.put("kcal", kcal); values.put("avg_speed", avgspeed); values.put("maximum_speed", maxspeed); values.put("date_time", datetime); values.put("synchronized", synchro); database.insertOrThrow(MyOpenHelper.TABLE_TRACKS, null, values); } public Cursor getTrack(int id) { String sid = Integer.toString(id); Cursor cursor = database.query(MyOpenHelper.TABLE_TRACKS, new String[] {"_id","date_time","distance","time", "kcal", "avg_speed", "maximum_speed"}, "_id = "+ sid, null, null, null, null); if (cursor != null) { cursor.moveToFirst(); } return cursor; } public Cursor getUnsynchronizedTracks() { Cursor cursor = database.query(MyOpenHelper.TABLE_TRACKS, new String [] {"*"}, "synchronized = 0", null, null, null, null); if (cursor != null) { cursor.moveToFirst(); } return cursor; } public void setSynchronizedTrack(int id) { ContentValues cv = new ContentValues(); cv.put("synchronized", 1); database.update(MyOpenHelper.TABLE_TRACKS, cv, "synchronized = 0", null); } public void updateUserStatistics(int dist, int max_dist, int time, int max_time, int kcal, int max_speed, int avg_speed) { ContentValues cv = new ContentValues(); cv.put("distance", dist); cv.put("max_distance", max_dist); cv.put("time", time); cv.put("max_time", max_time); cv.put("kcal", kcal); cv.put("max_speed", max_speed); cv.put("avg_speed", avg_speed); database.update(MyOpenHelper.TABLE_USERS, cv, null, null); } public Cursor getStats() { Cursor cursor = database.query(MyOpenHelper.TABLE_USERS, new String[] {"*"}, null, null, null, null, null); if (cursor != null) { cursor.moveToFirst(); } return cursor; } public void addFakeDataToTracks() { AddToTracks(1, 100, 2, 3, 5, 3, "2011-03-23 08:23:22", 0); AddToTracks(1, 240, 2, 3, 5, 3, "2011-04-13 08:34:06", 0); } public String getCurrentUser() { Cursor c = database.query(MyOpenHelper.TABLE_USERS, new String[] {"login"}, null, null, null, null, null); c.moveToFirst(); return c.getString(c.getColumnIndex("login")); } public void updateUser(String login) { ContentValues values = new ContentValues(); values.put("login", login); database.update(MyOpenHelper.TABLE_USERS, values, null, null); } public SQLiteDatabase getDatabase() { return database; }
public void sendData() { flag=0; String result = null; StringBuilder sb = null; String login = etLogin.getText().toString(); String password = etPassword.getText().toString(); if(login.length()!=0 && password.length()!=0) { dbHelper.open(); Cursor uTracks = dbHelper.getUnsynchronizedTracks(); for(int i=0; i < uTracks.getCount(); i++) { int id = uTracks.getInt(uTracks.getColumnIndex("_id")); String distance = uTracks.getString((uTracks.getColumnIndex("distance"))); String time = uTracks.getString((uTracks.getColumnIndex("time"))); String kcal = uTracks.getString((uTracks.getColumnIndex("kcal"))); String maximum_speed = uTracks.getString((uTracks.getColumnIndex("maximum_speed"))); String date_time = uTracks.getString((uTracks.getColumnIndex("date_time"))); nameValuePairs.add(new BasicNameValuePair("login", login)); nameValuePairs.add(new BasicNameValuePair("password", password)); nameValuePairs.add(new BasicNameValuePair("distance", distance)); nameValuePairs.add(new BasicNameValuePair("time", time)); nameValuePairs.add(new BasicNameValuePair("kcal", kcal)); nameValuePairs.add(new BasicNameValuePair("maximum_speed", maximum_speed)); nameValuePairs.add(new BasicNameValuePair("date_time", date_time)); uTracks.moveToNext(); Toast.makeText(this, "Synchronizacja tras biegu:\n" + i + "/" + uTracks.getCount(), 0).show(); try{ HttpClient httpclient = new DefaultHttpClient(); HttpPost httppost = new HttpPost("http://www.letsrun.pl/functions/put_tracks.php"); httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs)); HttpResponse response = httpclient.execute(httppost); HttpEntity entity = response.getEntity(); InputStream is = entity.getContent(); //log_err StringBuilder text = inputStreamToString(is); if(text.toString()=="log_err") { Toast.makeText(this, "Błędny login lub hasło!!!!", 0).show(); flag=1; } else { dbHelper.setSynchronizedTrack(id); dbHelper.updateUser(login); } } catch(Exception e) { Toast.makeText(this, "Brak połączenia z bazą.\n Jeżeli problem powtórzy się prosimy skontaktować się z administratorem!!!",0).show(); flag=1; } } } else { Toast.makeText(this, "Błędny login lub hasło!!!!", 0).show(); flag=1; } }
public void postData() { // Create a new HttpClient and Post Header JSONArray jArray; InputStream is = null; final TextView debbug = (TextView)findViewById(R.id.debbug); HttpClient httpclient = new DefaultHttpClient(); HttpPost httppost = new HttpPost("http://www.letsrun.pl/functions/get_stats.php"); try { String login = etLogin.getText().toString(); String password = etPassword.getText().toString(); List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(); nameValuePairs.add(new BasicNameValuePair("login", login)); nameValuePairs.add(new BasicNameValuePair("password", password)); httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs)); HttpResponse response = httpclient.execute(httppost); HttpEntity entity = response.getEntity(); is = entity.getContent(); StringBuilder text = inputStreamToString(is); //json parser JSONObject json_data=null; int distance; int max_distance; int time; int max_time; int kcal; int max_speed; int avg_speed; try{ jArray = new JSONArray(text.toString()); for(int i =0; i<jArray.length();i++) { json_data = jArray.getJSONObject(i); distance = json_data.getInt("distance"); max_distance = json_data.getInt("max_distance"); time = json_data.getInt("time"); max_time = json_data.getInt("max_time"); kcal = json_data.getInt("kcal"); max_speed = json_data.getInt("max_speed"); avg_speed = json_data.getInt("avg_speed"); dbHelper.updateUserStatistics(distance, max_distance, time, max_time, kcal, max_speed, avg_speed); } } catch(JSONException e1){ Toast.makeText(getBaseContext(), "Błędny login lub hasło", Toast.LENGTH_LONG).show(); flag=1; }catch (ParseException e1){ Toast.makeText(getBaseContext(), "Błędny aktualizacji", Toast.LENGTH_LONG).show(); flag=1; } } catch (ClientProtocolException e) { Toast.makeText(this, "Brak połączenia z bazą.\n Jeżeli problem powtórzy się prosimy skontaktować się z administratorem!!!", 0).show(); flag=1; } catch (IOException e) { Toast.makeText(this, "Brak połączenia z bazą.\n Jeżeli problem powtórzy się prosimy skontaktować się z administratorem!!!", 0).show(); flag=1; } if(flag==0){ Toast.makeText(this, "Synchronizacja wyników zakończona powodzeniem", 0).show(); } }
Baza danych serwisu internetowego została zaimplementowana z wykorzystaniem motoru bazy MySQL w wersji 5.1.55. Zapytania tworzące bazę, zostały zaprezentowane w projekcie logicznym.
Formularze wykorzystywane w projekcie, były stosowane w dwóch przypadkach: rejestracja oraz logowanie. Częściowo użyto również pól formularzy, przy wyborze przez użytkownika interwału czasowego dla wyświetlanych tras w połączeniu ze skryptem kalendarza JavaScript (Date Time Picker) z dostosowaniem formatu daty do składni SQL. Walidacja formularzy logowania i rejestracji była wykonana za dwupoziomowe: pomocą biblioteki jquery oraz jquery.validate oraz na poziomie PHP (w przypadku gdyby użytkownik miał wyłączoną obsługę JavaScript). Formularz logowania:
<form action="index.php" id="logForm" method="post"> <table> <tr> <td>Login:</td><td><input type="text" name="login" class="required" minlength="5"/></td> </tr> <tr> <td>Hasło:</td><td><input type="password" name="pass" class="required" minlength="5"/></td> </tr> <tr> <td></td> <td align="right"> <input type="submit" value="Zaloguj" class="btn"/> </td></tr> </table> </form>
Formularz rejestracji:
<form action="index.php" id="regForm" method="post"> Login: <input type="text" name="login" minlength="5" size="20"/> E-mail: <input type="text" name="email" class="required email" size="20"/> Hasło: <input id="password" type="password" name="pass" class="required" minlength="5" size="20"/> Potwierdź hasło: <input type="password" name="confirm_password" class="required" minlength="5" size="20"/> </br> <input type="submit" value="Zarejestruj" class="btn2"/> </form>
Wybór interesującego nas okresu:
Aby ułatwić zarządzanie tworzeniem i nadzorowaniem części wizualnej serwisu, wykorzystano system zarządzania treścią Drupal w wersji 6.20. Szablon strony wykonano w oparciu o CSS spinając go z systemem CMS za pomocą jego wewnętrznych zmiennych i znaczników.
Pierwszym zadaniem serwisu, była możliwość rejestracji i logowania nowych użytkowników. Rejestracja przebiegała dwuetapowo: po wypełnieniu pól formularza i walidacji z wykorzystaniem zarówno skryptu JavaScript jak i walidatora w PHP, użytkownik był wstawiany do bazy z polem aktywności ustawionym na nieaktywny. Pole to było aktualizowane dopiero po kliknięciu na link, który użytkownik otrzymywał w mailu. Wartość pola określona była przez kod md5. Funkcja realizująca rejestrację:
/** * Registers user with given login, email, password and db connection handler and sens confirmation mail. * @param string @login User login. * @param string @email User email. * @param string @password User password. * @param handler @dbDb connection handler. **/ functionregister_user($login,$email,$password,$db) { //Check login $result = $db->query('select * from users where login="'.$login.'"'); if(!$result) { echo '<p>Zapytanie nie powiodło się!</p>'; return false; } if($result->num_rows>0) { echo '<p>Wybierz inny login!</p>'; return false; } //Check email $result = $db->query('select * from users where email="'.$email.'"'); if(!$result) { echo '<p>Zapytanie nie powiodło się!</p>'; return false; } if($result->num_rows>0) { echo '<p>Ten mail jest już w użyciu!</p>'; return false; } //Register $md5 = md5(time()); $result = $db->query('INSERT INTO users (login,password,email,confirmed,distance,max_distance,time,max_time,kcal,max_kcal,avg_speed,max_speed) VALUES ("'.$login.'","'.$password.'","'.$email.'","'.$md5.'",0,0,0,0,0,0,0,0)'); if(!$result) { echo '<p>Rejestracja niemożliwa.</p>'; returnfalse; } else { echo '<p>Użytkownik <b>'.$login.'</b> został poprawnie zarejestrowany. Aby aktywować konto, kliknij na link przesłany na Twoją skrzynkę mailową</p>'; } $mail_content = "Wiadomość została automatycznie wygenerowana, ponieważ zarejestrowałeś się w serwisie Let'sRun.pl \n\nAby aktywować konto, kliknij w poniższy link: \n\nhttp://www.drupal.pisulak.pl/index.php?confirm=".$md5." \n\nJeżeli nie rejestrowałeś się w serwisie Let'sRun.pl zignoruj tą wiadomość, a konto zostanie usunięte w przeciągu 24h."; $mailr = mail($email, 'Let\'sRun - potwierdzeniezałożeniakonta', $mail_content,'From: rejestracja@letsrun.pl' . "\n"); if(!$mailr) { echo '<p>Nie można wysłać maila z potwierdzeniem!</p>'; } }
Oraz aktywację:
/** * Confirms user. * @param handler @dbDb connection handler. **/ functionconfirm_user($db) { if(!is_null($_GET['confirm'])) { $confirm = $_GET['confirm']; $result = $db->query('SELECT confirmed FROM users WHERE confirmed="'.$confirm.'"'); if(!$result) { echo '<p>Zapytanie nie powiodło się!</p>'; return false; } if($result->num_rows>0) { $result2 = $db->query('SELECT login FROM users WHERE confirmed="'.$confirm.'"'); $result = $db->query('UPDATE users SET confirmed="1" WHERE confirmed="'.$confirm.'"'); if($result) { $r = $result2->fetch_assoc(); echo '<p>Użtkownik<b>'.$r['login'].'</b> został potwierdzony.</p>'; } }else { echo '<p>Użytkownik został już potwierdzony, bądź nie istnieje w bazie.</p>'; returnfalse; } } }
Do logowania wykorzystano zmienną sesji:
/** * Authenticate user and create session **/ functionlog_user($login,$password,$db) { $result = $db->query('select login from users where login="'.$login.'"'); if(!$result) { return false; } if($result->num_rows>0) { $result = $db->query('select password from users where login="'.$login.'"'); if(!$result) { return false; } $table = $result->fetch_assoc(); if($table['password'] == $password) { $result = $db->query('select confirmed from users where login="'.$login.'"'); if(!result) { return false; } $table = $result->fetch_assoc(); if($table['confirmed'] == 1) { new_session($login); return true; } } } }
Pobieranie danych z bazy zależało w dużym stopniu do typu pobieranych danych. Przykładowa funkcja tworząca ranking dla skumulowanego dystansu:
/** * Creates output table with rank of summed up distances. **/ functionlongest_cumulated_distance() { $db = connect_letsrun(); $result = $db->query('SELECT login,distance FROM users ORDER BY distance DESC'); if(!$result) { echo 'Błąd zapytania.'; exit; } echo '<table>'; $rows = $result->num_rows; for($i=0;$i<$rows;$i++) { $row = $result->fetch_assoc(); $dis = $row['distance']/1000; echo '<tr><td>'.$row['login'].'</td><td>'.$dis.'</td></tr>'; } echo '</table>'; }
Serwis umożliwia także wybór interwału czasowego i oblicza dla niego statystyki. Jedna z opcji pozwalająca na określenie interwału przez użytkownika, wykorzystuje kalendarz JavaScript:
functionselect_run_period($start,$end) { $db = connect_letsrun(); if($end == 'week') { $result = $db->query('SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 7 DAY) AND NOW()'); }else if($end == 'twoweeks') { $result = $db->query('SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 14 DAY) AND NOW()'); }else if($end == 'month') { $result = $db->query('SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 30 DAY) AND NOW()'); }else { $result = $db->query('SELECT date_time, trackid FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN "'.$start.'" AND "'.$end.'"'); } if(!$result) { echo 'Błąd zapytania.'; exit; } if($result->num_rows == 0) { echo '<p>Brak tras w wybranym okresie.</p>'; return false; } echo '<table cellspacing="10">'; echo '<tr><td><b>Nr.</b></td><td><b>Data rozpoczęcia biegu</b></td><td><b>Godzina rozpoczęcia biegu</b></td></tr>'; $rows = $result->num_rows; for($i=0;$i<$rows;$i++) { $row = $result->fetch_assoc(); $j = $i+1; echo '<tr><td align="right"><a href="?q=node/4&track='.$row['trackid'].'">'.$j.'</a></td><td align="center">'.substr($row['date_time'],0,10).'</td><td align="center">'.substr($row['date_time'],11,8).'</td></tr>'; } echo '</table>'; if($end == 'week') { $result = $db->query('SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 7 DAY) AND NOW()'); }else if($end == 'twoweeks') { $result = $db->query('SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 14 DAY) AND NOW()'); }else if($end == 'month') { $result = $db->query('SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN DATE_SUB(NOW(),INTERVAL 30 DAY) AND NOW()'); }else { $result = $db->query('SELECT max(distance),avg(distance),max(time),avg(time),max(maximum_speed),sum(distance),sum(time) FROM tracks WHERE userid='.$_SESSION['luserid'].' AND date_time BETWEEN "'.$start.'" AND "'.$end.'"'); } if(!$result) { echo 'Błąd zapytania.'; exit; } echo '<table>'; $row = $result->fetch_assoc(); $dist = $row['sum(distance)']/1000; $t = $row['sum(time)']; $dis = $row['max(distance)']/1000; $dis2 = $row['avg(distance)']/1000; echo '<tr><td><b>Najdłuższydystans (km):</b></td><td align="right">'.$dis.'</td></tr>'; echo '<tr><td><b>Średnidystans (km):</b></td><td align="right">'.number_format($dis2, 2, '.', '').'</td></tr>'; echo '<tr><td><b>Najdłuższyczasbiegu (min):</b></td><td align="right">'.$row['max(time)'].'</td></tr>'; echo '<tr><td><b>Średniczasbiegu (min):</b></td><td align="right">'.number_format($row['avg(time)'], 2, '.', '').'</td></tr>'; echo '<tr><td><b>Najwyższaprędkość (km/h):</b></td><td align="right">'.$row['max(maximum_speed)'].'</td></tr>'; echo '<tr><td><b>Średniaprędkość (km/h):</b></td><td align="right">'.number_format($dist/min_to_h($t), 2, '.', '').'</td></tr>'; echo '</table>'; }
Jedną z funkcjonalności serwera, miała być możliwość integracji z klientem mobilnym dla systemu Android. Serwis umożliwia zdalną autoryzację i wstawianie tras do bazy z uwzględnieniem aktualizacji bazy użytkowników, wynikającej z ograniczeń technicznych, nie pozwalających na wykorzystanie triggerów. Funkcja odpowiadająca za wstawianie tras:
/** * Inserts new track into database and updates user fields **/ functionconnect_and_update() { $login = $_POST['login']; $password = $_POST['password']; $distance = $_POST['distance']; $time = $_POST['time']; $kcal = $_POST['kcal']; $maximum_speed = $_POST['maximum_speed']; $date_time = $_POST['date_time']; $userid = get_user_id($login,$password); if(!$userid) { echo "log_err"; return false; } else { if(update_tables($userid,$distance,$time,$kcal,$maximum_speed,$date_time)) { echo "ok"; return false; } else { echo "log_err"; return true; } } }
Oraz ich pobieranie:
/** * Returns data from users table **/ functionreturn_stats() { $login = $_POST['login']; $password = $_POST['password']; $userid = get_user_id($login,$password); if(!$userid) { echo "log_err"; return false; } else { $db = connect_letsrun(); $result = $db->query('SELECT distance, max_distance, time, max_time, kcal, max_speed, avg_speed FROM users WHERE userid="'.$userid.'"'); if(!$result) { echo "log_err"; return false; } $value = $result->fetch_assoc(); $output[]=$value; print(json_encode($output)); return true; } }
Do przesyłania danych pomiędzy serwisem a klientem, wykorzystano format JSON.
W przyszłości, planowane jest dołączanie kolejnych funkcjonalności do serwisu internetowego. Jedną z nich jest wizualne prezentowanie danych na temat tras i okresów (np. w postaci czytelny wykresów), co mogłoby podnieść atrakcyjność strony. Dobrym pomysłem, jest również dodanie więcej informacji o zalogowanym użytkowniku, takich jak: miejsce zamieszkania, wiek, awatar itp. Jedną z ważniejszych, planowanych funkcjonalności, jest dodanie możliwości zapisu tras na mapach Google, co wymaga jednak bezpośredniego sprzężenia i informacji od aplikacji mobilnej. Od strony marketingowej, planowane jest stworzenie profilu na FB oraz dalsze prace nad szatą graficzną.
Baza danych aplikacji mobilnej została zaimplementowana z wykorzystaniem motoru bazy SQLite. Zapytania tworzące bazę, zostały zaprezentowane w projekcie logicznym.
Formularze wykorzystywane w aplikacji mobilnej w dwóch przypadkach: przesyłania wyników biegów na serwer oraz w czasie podglądania ich na telefonie. Walidacja formularza wysyłania wyników na serwer została podzielona na dwa etapy sprawdzenia zawartości pól przez aplikację oraz sprawdzenie statusu logowania na serwer (implementacja zawarta w linki do pól w opisie logicznym).
Aplikacja służąca do śledzenia parametrów biegu użytkownika składa się z: Menu Aplikacji pozwalające na nawigację po programie:
Menu wyboru trybu biegu i ekran śledzący:
W menu wyboru biegu użytkownik może wybrać jeden z trzech typów biegu (swobodny, na czas i na dystans), po kliknięci w przycisk start następuje przejście do ekranu śledzenia biegu.
W tym momencie aplikacja próbuje nawiązać połączenie z satelitami systemu GPS, gdy do tego dojdzie, przycisk Start staje się aktywny i użytkownik może rozpocząć pomiar swoich statystyk, które po zakończeniu biegu zostają zapisane w bazie danych aplikacji.
@Override publicvoidonFinish() { dbHelper.open(); int kcal = (int) ((mass*speed*speed)/(2*4.18)); Calendar cal = newGregorianCalendar(); String date = cal.get(Calendar.YEAR)+"-"+cal.get(Calendar.MONTH)+"-"+cal.get(Calendar.DAY_OF_MONTH)+"-"+" "+cal.get(Calendar.HOUR_OF_DAY)+":"+cal.get(Calendar.MINUTE)+":"+cal.get(Calendar.SECOND); dbHelper.AddToTracks(1, (int) distance_count, time, kcal, (int) speed, speed_max, date, 0); }
Menu podglądu wyników, w tym miejscu użytkownik może przeglądać swoje osiągnięcia w postaci statystyk (ściągniętych z serwera aplikacji webowej) jak i również biegów które odbył w ostatnim czasie.
private Cursor getScores() { Cursor cursor = dbHelper.getDatabase().query("tracks", FROM_TRACKS, null , null , null ,null , ORDER_BY); startManagingCursor(cursor); return cursor; } privatevoidsetStats() { Cursor cTrack = dbHelper.getStats(); startManagingCursor(cTrack); tvDistance.setText(cTrack.getString(cTrack.getColumnIndex("distance")) + "[m]"); tvMaxDistance.setText(cTrack.getString(cTrack.getColumnIndex("max_distance")) +" [m]"); tvTime.setText(cTrack.getString(cTrack.getColumnIndex("time")) + " [min]"); tvMaxTime.setText(cTrack.getString(cTrack.getColumnIndex("max_time")) + " [min]"); tvKcal.setText(cTrack.getString(cTrack.getColumnIndex("kcal")) + " [kcal]"); tvMaxKcal.setText(cTrack.getString(cTrack.getColumnIndex("max_kcal")) + " [kcal]"); tvAvgSpeed.setText(cTrack.getString(cTrack.getColumnIndex("avg_speed")) + " [km/h]"); }
W najbliższym okresie planowana jest przebudowa aplikacji tak by, przy jej pomocy możliwe było zapis przebiegu tras i prezentacja ich na mapach Google, jak przesłanie na serwer aplikacji webowej.
Pisanie pod system operacyjny Android ma dwa aspekty. Po pierwsze można w bardzo łatwy sposób wdrożyć się w to środowisko gdyż w całości oparte jest na języku JAVA i XML służącemu do modelowania wyglądu graficznego aplikacji (co bardzo ułatwia prace nad aplikacją). Dodatkowym atutem jest również mocne wsparcie producenta dla swojego systemu- firmy Google, jak i środowiska programistów piszących na tą platformę. Jednak nadal poważnym problemem pozostaje szybko zmieniające się specyfikacja środowiska i brak dobrych edytorów do edycji warstwy wizualnej aplikacji. Poważnym problemem jest również bardzo „ciężki” dla systemów operacyjnych emulator telefonów opartych na Androidzie. Emulator ma również tendencje do zniekształcania wyglądu aplikacji, który po zainstalowaniu na rzeczywistym telefonie prezentuje się całkowicie prawidłowo.
http://www.letsrun.pl/ - serwis internetowy (wersja beta, bez możliwości pobrania aplikacji mobilnej)