תרגיל בפיתוח אפליקציה: מי תזכה במונדיאל הטוויטים, ארגנטינה או גרמניה?

ככה נראת אפליקציה שיודעת בדיוק מה קורה על המגרש רק באמצעות מעקב בזמן אמת אחרי ציוצים בטוויטר

maps

הפרויקט נבנה כחלק מהאקטון Fuesday שערכה חברת טיקל בחודש יוני האחרון. חברי הצוות היו: שי מסיסטרנו, אולגה כצנלסון, גייל שטייגר ודמיטרי קרסננקו.

לרגל המונדיאל, החלטנו להציג את תוצאות המשחקים בצורה מעניינת. התוצאה היא אפליקציה שמדמה בזמן-אמת את משחקי המונדיאל.

כך זה עובד: על המסך מוצגת מפת כדור-הארץ. בכל פעם שמישהו מצייץ בקשר לאחת משתי הנבחרות המשחקות, המיקום שלה במפה מהבהב. זה יכול להיות אזכור שם המדינה כחלק מהציוץ, משם המצייץ ואפילו כ-Hash Tag. כל אחד מאלה ״ידליק״ את המפה וימחיש באופן ויזואלי את התפתחות המשחק. כן, זוהי חווית משחק נוספת. אפשר לשבת מול האפליקציה ולראות כיצד המשחק מתפתח, אפילו מבלי לצפות בו! כל גול או מהלך מסוכן מן הסתם זוכים לאלפי ציוצים, כך שהאפליקציה ממחישה בזמן-אמת את המתרחש על המגרש.

וכעת, לצד הטכני

בצד שרת השתמשנו בטכנולוגיות: Spring-boot, Spring-data, MongoDb. האפליקציה ממפה את הציוצים למדינה, סופרת את מקורות הציוץ ועוקבת אחר הדרכים הפופולריות לצייץ (דרך אנדרואיד, ווב, אייפון וכו׳).

ארכיטקטורה:

צד-השרת מחולק לשני חלקים בלתי-תלויים אחד בשני. אחד לצורך אנליטיקות והאחר – עבור תקשורת REST עם צד-הלקוח.

שרת האנליטיקות:

שרת ה-Spark אחראי לאיסוף הציוץ, סימונו, ספירת מקורותיו ושמירתם ב-MongoDB. הדרך בה זה מתבצע:

1. קריאת מקור המידע.
2. סינון הציוצים לפי רשימת מדינות (אשר הגדרנו מראש) ומיפוים:

[java]
<country, List<Tweets>>
[/java]

3. המרת ה-JSON לאובייקט.
4. המרת המידע לפורמט הרצוי.

[java]
val sc = new SparkContext(conf)
val tweets = sc.textFile(path).fatMap(tweet => {
//Create tuples by wanted tags
WorldCupDictionary.DICTIONARY.flter(tag => tweet.toLowerCase.contains(tag)).map(tag =>
(tag, tweet))
}).map(tuple => {
//Convert JSON string to a tweet object
(tuple._1, WorldCupDictionary.MAPPER.readValue(tuple._2,
classOf[Object2ObjectOpenHashMap[String, Object]]))
}).map(tuple => {
//Convert tweet object to a world cup object
val map = new BasicBSONObject()
map.put("country", tuple._1)
map.put("twit", tuple._2("text").toString)
map.put("source", tuple._2("source").toString.replaceAll("(<a href=\"(.*)\">)|(</a>)", ""))
map.put("userName", tuple._2("user").asInstanceOf[java.util.Map[String,String]]("screen_name"))
try {
map.put("dateInt",
WorldCupDictionary.CUP_DATE_FORMAT.format(WorldCupDictionary.TWITTER_DATE_FORMAT.pars
e(tuple._2("created_at").toString)))
} catch { case ex: Exception => println(ex)}
map
}).cache()
[/java]

כעת, ניתן לשמור את המידע.

שרת REST:

שרת זה יכול להימצא במכונה שונה ותכליתו היחידה היא לקרוא את המידע מבסיס הנתונים ולשלוח אותו לצד-הלקוח. לכן, בחרנו את הטכנולוגיה הפשוטה ביותר אשר תמלא את התכלית והיא: Spring-boot, Spring-data.

Sprint-boot מייצר את שלד הפרויקט ומספק פתרון REST API מלא.

Spring-data מייצר עבורנו את השאילתות הנדרשות.

לדוגמה:

[java]
@Repository
public interface TweetRepository extends MongoRepository<Tweet,String>{
}
@Service
public class TweetService {
@Autowired
private TweetRepository repository;
public void save(Tweet twit) {
repository.save(twit);
}
public List<Tweet> fndAll() {
return repository.fndAll();
}
}
[/java]

דוגמה ל-REST API:

[java]
@RequestMapping(value="/pie", method=RequestMethod.GET)
public @ResponseBody List<Pie> providePieInfo() {
return pieService.fndAll();
}
@RequestMapping(value="/game/germany", method=RequestMethod.GET)
public @ResponseBody List<Game> provideGermanyInfo(@RequestParam(value="start",
required=false) Long start, @RequestParam(value="fnish", required=false) Long fnish) {
if(start!= null && fnish != null){
return gameService.fndByCountryAndIntDate("germany", start, fnish);
}
return gameService.fndByCountry("germany");
[/java]

תוצאה:

5 המקורות הפופולריים לציוצים (בהם המצייצים הזכירו את שם המדינה):

1
וכמה אחרים:
2
דוגמה לציוצים שעלו בתוצאות:
3

תקשורת עם צד-הלקוח:

צד-הלקוח מציין את שם המדינה עבורה הוא מבקש ציוצים, ואת הזמן בהם הם נכתבו (בפורמט של שנה, חודש, יום, שעה, דקה ושנייה – משורשרים לידי מחרוזת אחת גדולה).

צד הלקוח:

בצד השרת השתמשנו ב-Gulp, מנהל משימות דומה ל-Grunt. השימוש בו היה לצורך תהליך דמוי-קומפילציה, של צמצום הקבצים בהם עשינו שימוש, ובהאצת זמן הפיתוח על-ידי משימת Watch אשר עוקבת אחר שינויים בקוד ו״מקמפלת״ את הפרויקט בתגובה לכך:

[javascript]
var gulp = require(‘gulp’),
concat = require(‘gulp-concat’);

gulp.task(‘js’, function () {
gulp
.src([
‘./js/libs/d3.v3.min.js’,
‘./js/libs/topojson.v1.min.js’,
‘./js/libs/datamaps.world.min.js’,
‘./js/main.js’
])
.pipe(concat(‘all.js’))
.pipe(gulp.dest(‘./public/js’));
});

gulp.task(‘default’, function () {
gulp.run(‘js’);
});

gulp.watch(‘./js/**’, function () {
gulp.run(‘js’);
});
[/javascript]

לצורך הצגת מפת כדור-הארץ, השתמשנו בספריית קוד-פתוח, בשם DataMaps. הספרייה מציגה SVG ומבוססת על ספריית הויזואליזציה D3.js.

קוד ה-JS שלנו מבצע קריאת GET לשרת ה-REST שהזכרנו קודם-לכן. הדבר נעשה מדי שנייה והוא כולל בקשה לקבלת מידע אודות שתי המדינות בנפרד:

[javascript]
var follow = function (homeCountry, awayCountry) {
setInterval(function () {
var now = {},
startTime = {},
endTime = {};

now = getTime(new Date());
showClock(now);

gameTime.setSeconds(gameTime.getSeconds() + 1);
startTime = getTime(gameTime);
startTime = startTime.year + startTime.month + startTime.day + startTime.hours + startTime.minutes + startTime.seconds;
gameTime.setSeconds(gameTime.getSeconds() + 1);
endTime = getTime(gameTime);
endTime = endTime.year + endTime.month + endTime.day + endTime.hours + endTime.minutes + endTime.seconds;

xhrGet(xhrHomeCountry, SETTINGS.API_URL + ‘/’ + homeCountry + ‘?start=’ + startTime + ‘&finish=’ + endTime, checkResponse);
xhrGet(xhrAwayCountry, SETTINGS.API_URL + ‘/’ + awayCountry + ‘?start=’ + startTime + ‘&finish=’ + endTime, checkResponse);
}, SETTINGS.FOLLOWING_INTERVAL)
}
[/javascript]

בסביבת ייצור פתרון שכזה יביא לקריסת השרת, לכן קיימות שתי חלופות:
1. לעבוד מול שרת NodeJS, תוך שימוש ב-Web Sockets לתקשורת רציפה עם השרת.
2. לעבוד מול שרת אחר ולשלוח אליו בקשה מדי מספר שניות תוך השהיית החזרת התשובה ממנו (Long
Polling(

במידה והשרת החזיר תשובה מלאה (כלומר – קיים ציוץ הקשור לאחת הקבוצות המשחקות), מופעלת אנימציית הבהוב:

[javascript]
var highlight = function (country, fromColor, toColor) {
var _this = this,
choropleth = {},
from = {},
to = {},
original = {};

from[country] = fromColor;
to[country] = toColor;
original[country] = SETTINGS.DEFAULT_FILL;

console.log("animation", animation);
var animation = setInterval(function () {
choropleth = (choropleth === from) ? to : from;
_this.map.updateChoropleth(choropleth);
}, SETTINGS.ANIMATION_INTERVAL);
setTimeout(function () {
console.log("animation", animation);
clearInterval(animation);
_this.map.updateChoropleth(original); // Roll back to default color
}, SETTINGS.ANIMATION_TIME)
}
[/javascript]

גם כאן ניתן לבצע אופטימיזציה, בדמות העברת האנימציה לפורמט CSS3. במקרה הנוכחי, פתרון זה איננו אפשרי מפני שספריית הויזואליזציה אינה תומכת בכך.

ולתוצאה הסופית:
צפו בסימולציה של המשחק בין ארגנטינה לגרמניה

גיט הפרויקט:
https://github.com/OlgaKatzenelson/worldcup/

4

Avatar

שי מסיסטרנו

מפתח ווב מאז 2007, בעברו מרצה במכללת Ness מ-2009 עד 2012.

הגב

1 תגובה על "תרגיל בפיתוח אפליקציה: מי תזכה במונדיאל הטוויטים, ארגנטינה או גרמניה?"

avatar
Photo and Image Files
 
 
 
Audio and Video Files
 
 
 
Other File Types
 
 
 

* היי, אנחנו אוהבים תגובות!
תיקונים, תגובות קוטלות וכמובן תגובות מפרגנות - בכיף.
חופש הביטוי הוא ערך עליון, אבל לא נוכל להשלים עם תגובות שכוללות הסתה, הוצאת דיבה, תגובות שכוללות מידע המפר את תנאי השימוש של Geektime, תגובות שחורגות מהטעם הטוב ותגובות שהן בניגוד לדין. תגובות כאלו יימחקו מייד.

סידור לפי:   חדש | ישן | הכי מדורגים
דין
Guest

כמו תמיד, חיקוי זול של משהו שקיים הרבה מאוד זמן בעולם…
עשו את זה לסופרבול, עשו את זה לכל המשחקים במונדיאל…
וזה נראה בהרבה יותר טוב ממה ששמתם פה.
להלן דוגמה:
http://srogers.cartodb.com/viz/1b9b0670-8d15-11e3-8ddf-0edd25b1ac90/embed_map

wpDiscuz

תגיות לכתבה: