Ceux qui sont passé par ici auront remarqué que la peinture est encore fraîche. Les travaux ne sont pas finis, mais l'essentiel est là. Cette mise à jour marque un autre changement: j'ai quitté Netvibes pour rejoindre une jeune société, Comuto. Désormais, mon travail sera donc surtout visible sur http://www.covoiturage.fr/, le premier site de covoiturage en France. En coulisse, j'ai d'autres projets en cours, mais j'aurais l'occasion de vous reparler le moment venu.
Maurice Svay
Prendre régulièrement une photo avec son Mac et l'uploader
Par Maurice Svay le mercredi, février 17 2010, 12:44 - Technologie Tweet
Et si on prenait des photos à intervalles réguliers et qu'on les mettait en ligne? Pourquoi faire? Pour faire de la vidéo surveillance, monter un film stop-motion ou retrouver qui a volé votre Mac par exemple. Si vous avez un Mac équipé d'une webcam, c'est facile.
Prendre des photos en ligne de commande
Tout d'abord, téléchargez ImageSnap et mettez-le dans /usr/local/bin par exemple. N'importe quel dossier dans votre PATH fera également l'affaire.
Uploader l'image vers un serveur
Ensuite, créez un fichier snap.sh avec le code suivant:
#!/bin/sh upload="http://example.com/upload.php" imagesnap snapshot.jpg curl -F "photo=@snapshot.jpg" $upload
Vous pouvez mettre de dossier où vous voulez, dans votre dossier personnel par exemple. Ce script permet de prendre une photo, et de l'uploader vers http://example.com/upload.php en POST avec CURL.
Récuperer l'image uploadée et la stocker
Maintenant, mettez sur votre serveur le script PHP upload.php suivant. Il permet de récupérer l'upload et de stocker l'image sur le serveur.
Créez également un dossier img à côté de votre script. C'est dans ce dossier que seront mises les photos.
<?php
$path = dirname(__FILE__).'/img';
$year_folder = $path . '/' . date('Y');
if (!is_dir($year_folder)) {
mkdir($year_folder);
}
$month_folder = $year_folder . '/' . date('m');
if (!is_dir($month_folder)) {
mkdir($month_folder);
}
$day_folder = $month_folder . '/' . date('d');
if (!is_dir($day_folder)) {
mkdir($day_folder);
}
$file_path = $day_folder . '/' . date('Ymd-His') . '_' . $_SERVER['REMOTE_ADDR'] . '.jpg';
if (move_uploaded_file($_FILES['photo']['tmp_name'], $file_path)) {
echo "OK\n";
} else {
echo "Error\n";
}
Notez que les photos seront stockées dans des dossiers comme dans cet exemple: img/2010/02/17/20100210-120000_127.0.0.1.jpg. Le fichier comporte donc la date, l'heure et l'IP d'où la photo est envoyée.
Automatiser la tâche
Une fois que tout est en place, il suffit d'exécuter snap.sh pour prendre la photo et l'uploader. Pour automatiser ça, on peut ajouter cette action dans cron. Pour cela, faites :
crontab -e
et ajoutez la ligne suivante (à adapter selon l'emplacement de snap.sh):
0 * * * * /Users/mauricesvay/snap.sh
Voilà. Votre mac prendra des photos toutes les heures pour les mettre sur votre serveur. Facile.
Ne ratez pas la conférence Paris Web 2009, du 8 au 10 octobre 2009!
Par Maurice Svay le jeudi, juillet 9 2009, 13:32 - Webstandards Tweet

Comme chaque année depuis 2006, a lieu Paris Web, LA conférence qui s'adresse à ceux qui font le Web. Contrairement à d'autres conférences, Paris Web est une conférence réellement utile, où on apprend des choses. On y retrouve bon nombre d'experts pour discuter des sujets qui font le Web: technologies, design, accessibilité, standards, etc. Cette année encore, on retrouvera des têtes francophones connues comme Tristan Nitot, Daniel Glazman ou Amélie Boucher, mais également des orateurs internationaux comme Molly Holzschlag ou Nicole Sullivan. Tous les témoignages sont d'accord, Paris Web est l'une des meilleures conférences sur le sujet. Et pourtant, le tarif est très abordable. Tout ça pour dire qu'il ne faut pas rater cette conférence, donc foncez VOUS INSCRIRE!
Face detection in pure PHP (without OpenCV)
Par Maurice Svay le vendredi, juin 19 2009, 12:17 - Technologie Tweet
Une résumé en français est disponible en fin d'article.
Lately, I've been looking for ways to detect faces in photos with PHP. Nowadays, face detection is built in many consumer products (camera obviously, but also Google and iPhoto), and seems to be a pretty common job. So I expected to find many solutions for doing it with PHP. Surprisingly, the only one I could find is OpenCV, an opensource lib that was originally developed by Intel. OpenCV seems to perform well but you need to be able to install it on your server. In my case, I wanted to have a pure PHP solution, so it can work with most hosts.
Learning about face detection
So I started to think about implementing it myself. I read a articles, scientific papers, etc. The website http://www.facedetection.com/ is a great resource by the way. From this short research, I learnt that one of the most popular solutions is to use Viola Jones training with a Haar classifier. Very informative, but tedious to implement. I'm lazy, you know.
Always look at what others are doing
Then I looked for existing implementations in other languages. Let's say Flash and Javascript. With Canvas, Javascript developers will certainly look at what flash developers do. There seem to be a few libs for face detection in AS3. They even work in real time. Pretty cool. I kept searching and finally found a canvas+javascript implementation of face detection at http://blog.kpicturebooth.com/?p=8. The code looked fairly compact and simple. Shouldn't be hard to port to PHP.
The code
Once the code converted to PHP, here's the result:
<?php
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// @Author Karthik Tharavaad
// karthik_tharavaad@yahoo.com
// @Contributor Maurice Svay
// maurice@svay.Com
class Face_Detector {
protected $detection_data;
protected $canvas;
protected $face;
private $reduced_canvas;
public function __construct($detection_file = 'detection.dat') {
if (is_file($detection_file)) {
$this->detection_data = unserialize(file_get_contents($detection_file));
} else {
throw new Exception("Couldn't load detection data");
}
//$this->detection_data = json_decode(file_get_contents('data.js'));
}
public function face_detect($file) {
if (!is_file($file)) {
throw new Exception("Can not load $file");
}
$this->canvas = imagecreatefromjpeg($file);
$im_width = imagesx($this->canvas);
$im_height = imagesy($this->canvas);
//Resample before detection?
$ratio = 0;
$diff_width = 320 - $im_width;
$diff_height = 240 - $im_height;
if ($diff_width > $diff_height) {
$ratio = $im_width / 320;
} else {
$ratio = $im_height / 240;
}
if ($ratio != 0) {
$this->reduced_canvas = imagecreatetruecolor($im_width / $ratio, $im_height / $ratio);
imagecopyresampled($this->reduced_canvas, $this->canvas, 0, 0, 0, 0, $im_width / $ratio, $im_height / $ratio, $im_width, $im_height);
$stats = $this->get_img_stats($this->reduced_canvas);
$this->face = $this->do_detect_greedy_big_to_small($stats['ii'], $stats['ii2'], $stats['width'], $stats['height']);
$this->face['x'] *= $ratio;
$this->face['y'] *= $ratio;
$this->face['w'] *= $ratio;
} else {
$stats = $this->get_img_stats($this->canvas);
$this->face = $this->do_detect_greedy_big_to_small($stats['ii'], $stats['ii2'], $stats['width'], $stats['height']);
}
return ($this->face['w'] > 0);
}
public function toJpeg() {
$color = imagecolorallocate($this->canvas, 255, 0, 0); //red
imagerectangle($this->canvas, $this->face['x'], $this->face['y'], $this->face['x']+$this->face['w'], $this->face['y']+ $this->face['w'], $color);
header('Content-type: image/jpeg');
imagejpeg($this->canvas);
}
public function toJson() {
return "{'x':" . $this->face['x'] . ", 'y':" . $this->face['y'] . ", 'w':" . $this->face['w'] . "}";
}
public function getFace() {
return $this->face;
}
protected function get_img_stats($canvas){
$image_width = imagesx($canvas);
$image_height = imagesy($canvas);
$iis = $this->compute_ii($canvas, $image_width, $image_height);
return array(
'width' => $image_width,
'height' => $image_height,
'ii' => $iis['ii'],
'ii2' => $iis['ii2']
);
}
protected function compute_ii($canvas, $image_width, $image_height ){
$ii_w = $image_width+1;
$ii_h = $image_height+1;
$ii = array();
$ii2 = array();
for($i=0; $i<$ii_w; $i++ ){
$ii[$i] = 0;
$ii2[$i] = 0;
}
for($i=1; $i<$ii_w; $i++ ){
$ii[$i*$ii_w] = 0;
$ii2[$i*$ii_w] = 0;
$rowsum = 0;
$rowsum2 = 0;
for($j=1; $j<$ii_h; $j++ ){
$rgb = ImageColorAt($canvas, $j, $i);
$red = ($rgb >> 16) & 0xFF;
$green = ($rgb >> 8) & 0xFF;
$blue = $rgb & 0xFF;
$grey = ( 0.2989*$red + 0.587*$green + 0.114*$blue )>>0; // this is what matlab uses
$rowsum += $grey;
$rowsum2 += $grey*$grey;
$ii_above = ($i-1)*$ii_w + $j;
$ii_this = $i*$ii_w + $j;
$ii[$ii_this] = $ii[$ii_above] + $rowsum;
$ii2[$ii_this] = $ii2[$ii_above] + $rowsum2;
}
}
return array('ii'=>$ii, 'ii2' => $ii2);
}
protected function do_detect_greedy_big_to_small( $ii, $ii2, $width, $height ){
$s_w = $width/20.0;
$s_h = $height/20.0;
$start_scale = $s_h < $s_w ? $s_h : $s_w;
$scale_update = 1 / 1.2;
for($scale = $start_scale; $scale > 1; $scale *= $scale_update ){
$w = (20*$scale) >> 0;
$endx = $width - $w - 1;
$endy = $height - $w - 1;
$step = max( $scale, 2 ) >> 0;
$inv_area = 1 / ($w*$w);
for($y = 0; $y < $endy ; $y += $step ){
for($x = 0; $x < $endx ; $x += $step ){
$passed = $this->detect_on_sub_image( $x, $y, $scale, $ii, $ii2, $w, $width+1, $inv_area);
if( $passed ) {
return array('x'=>$x, 'y'=>$y, 'w'=>$w);
}
} // end x
} // end y
} // end scale
return null;
}
protected function detect_on_sub_image( $x, $y, $scale, $ii, $ii2, $w, $iiw, $inv_area){
$mean = ( $ii[($y+$w)*$iiw + $x + $w] + $ii[$y*$iiw+$x] - $ii[($y+$w)*$iiw+$x] - $ii[$y*$iiw+$x+$w] )*$inv_area;
$vnorm = ( $ii2[($y+$w)*$iiw + $x + $w] + $ii2[$y*$iiw+$x] - $ii2[($y+$w)*$iiw+$x] - $ii2[$y*$iiw+$x+$w] )*$inv_area - ($mean*$mean);
$vnorm = $vnorm > 1 ? sqrt($vnorm) : 1;
$passed = true;
for($i_stage = 0; $i_stage < count($this->detection_data); $i_stage++ ){
$stage = $this->detection_data[$i_stage];
$trees = $stage[0];
$stage_thresh = $stage[1];
$stage_sum = 0;
for($i_tree = 0; $i_tree < count($trees); $i_tree++ ){
$tree = $trees[$i_tree];
$current_node = $tree[0];
$tree_sum = 0;
while( $current_node != null ){
$vals = $current_node[0];
$node_thresh = $vals[0];
$leftval = $vals[1];
$rightval = $vals[2];
$leftidx = $vals[3];
$rightidx = $vals[4];
$rects = $current_node[1];
$rect_sum = 0;
for( $i_rect = 0; $i_rect < count($rects); $i_rect++ ){
$s = $scale;
$rect = $rects[$i_rect];
$rx = ($rect[0]*$s+$x)>>0;
$ry = ($rect[1]*$s+$y)>>0;
$rw = ($rect[2]*$s)>>0;
$rh = ($rect[3]*$s)>>0;
$wt = $rect[4];
$r_sum = ( $ii[($ry+$rh)*$iiw + $rx + $rw] + $ii[$ry*$iiw+$rx] - $ii[($ry+$rh)*$iiw+$rx] - $ii[$ry*$iiw+$rx+$rw] )*$wt;
$rect_sum += $r_sum;
}
$rect_sum *= $inv_area;
$current_node = null;
if( $rect_sum >= $node_thresh*$vnorm ){
if( $rightidx == -1 )
$tree_sum = $rightval;
else
$current_node = $tree[$rightidx];
} else {
if( $leftidx == -1 )
$tree_sum = $leftval;
else
$current_node = $tree[$leftidx];
}
}
$stage_sum += $tree_sum;
}
if( $stage_sum < $stage_thresh ){
return false;
}
}
return true;
}
}
And you simply use the class this way:
$detector = new Face_Detector('detection.dat');
$detector->face_detect('maurice_svay_150.jpg');
$detector->toJpeg();
Which gives the following result:

The code requires GD and is a bit slow but should work on most PHP servers. You'll also need the data file: http://svay.com/experiences/face-detection/detection.dat. Let me know if you have ideas for improving the code :)
Résumé en français
J'ai récemment cherché une solution pour faire de la détection de visage en PHP. Après avec écarté plusieurs solutions qui ne sont pas facilement utilisable sur la plupart des hébergeurs, j'ai finalement porté du code Javascript+Canvas vers PHP+GD. Le résultat une class PHP qui est un peu lente, mais qui fonctionne sur la plupart des serveurs PHP. Si vous avez des idées pour améliorer le code, n'hésitez-pas!
Générer des anaglyphes avec javascript et canvas
Par Maurice Svay le vendredi, avril 3 2009, 23:55 - Webstandards Tweet
Il n'y a pas très longtemps, j'ai trouvé des lunettes 3D sur une boîte de Chocapics. Ca m'a rappelé les essais d'images en 3D que je faisais il y a quelques années. La recette est super simple: on prend deux images, une pour l'oeil gauche et l'autre pour l'oeil droit. De l'une, on prend la composante rouge et de l'autre, la composante cyan (bleu+vert). On mélange le tout et... paf! Ca fait un anaglyphe!
Ayant joué récemment avec Javascript et l'élément canvas, je me suis dit que ça pourrait se faire facilement avec ces deux technologies. Après quelques essais, j'ai obtenu un Anaglyph generator qui fonctionne à peu près avec un Firefox, un Safari ou un Opera récent. Concernant Internet Explorer, aucune chance pour le moment: excanvas ne supporte pas encore getImageData ni putImageData.
Pour générer un anaglyphe, il suffit d'indiquer deux URLs d'images et de presser le bouton. Pour simplifier, le resultat est toujours affiché en 640x480 (N'hésitez pas à poster l'URL de votre anaglyphe dans les commentaires!).

Ceux que ça intéresse, peuvent jeter un oeil au code. C'est plutôt simple:
- on charge deux images;
- on attend qu'elles soient complètement chargées;
- on crée deux canvas pour y copier les images;
- on remplace la composante rouge d'un canvas par la composante rouge de l'autre;
- et on affiche le résultat!
Les pièges à éviter
Même si la technique est simple, il y'a tout de même quelques pièges à contourner.
Le premier est la politique de sécurité du navigateur. Comme pour les requêtes XMLHttpRequest, le navigateur ne permet d'accéder aux pixel que des images du même domaine. Du coup, pour pouvoir afficher une image venant d'ailleurs, il faut passer un proxy.
Voilà à quoi ressemble le proxy:
<?php
if (isset($_GET['url']) && preg_match('/^http/i', $_GET['url'])) {
$url = $_GET['url'];
if (preg_match('/.png$/i', $url)) {
header('Content-type: image/png');
}elseif (preg_match('/.jpg$/i', $url)) {
header('Content-type: image/jpg');
}
readfile($url);
}
Le second, est qu'il faut s'assurer que les images sont bien chargées. En local, ça va forcément très vite et on n'a jamais de problème. Avec des images distantes, ça peut mettre un peu de temps et empêcher le script de fonctionner. Pour résoudre ce problème, deux trucs:
- utiliser setTimeout pour attendre en boucle que les images soient chargées. La fonction
Anaglyph.waitvérifie si les images sont totalement chargée en regardant l'attributcomplete. Si ce n'est pas le cas, elle va attendre un peu et s'appeler elle-même à nouveau. Une fois que les deux images sont chargées, elle peut appelerAnaglyph.makequi va générer l'anaglyphe. - imbriquer le code qui manipules les canvas dans un
try/catchpour éviter que le code ne casse.
Si l'élément canvas vous intéresse, il y'a déjà pas mal de ressources intéressantes sur le sujet:
- L'élément Canvas sur DevMo
- Les posts parlant de Canvas de Jacob Seidelin
- en particulier, la cheat sheet Canvas
- Les articles de Dev.Opera sur Javascript
Plein de choses intéressantes ont déjà été faites avec canvas: de la 3D, de la visualisation de musique, de la retouche d'images, des trucages vidéos, etc. Il y'a sûrement encore plein d'autres choses à faire. De quoi me faire réfléchir toute la nuit encore...
Concours Mozilla / Dotclear
Par Maurice Svay le mardi, mars 24 2009, 18:20 - Général Tweet
Du 23 mars au 16 mai 2009 se déroule un concours de thèmes Dotclear2, organisé conjointement par Mozilla Europe, Dotclear et DotAddict.org.

Il y a plusieurs prix à gagner:
- 1er : une tablette graphique Wacom Intuos3 A5 Wide
- 2ème : une sonde de calibration d'écran Spyder 2 Express
- 3ème : un super sac Firefox
Les trois gagnants recevront aussi un livre « CSS2 Pratique du design web », dédicacé par son auteur Raphaël Goetter. Et pour les dix finalistes, un T-shirt Mozilla sera offert.
Les meilleurs thèmes seront choisis par un jury composé de Tara Shahian (Mozilla), Olivier Meunier (Dotclear), Anne Cavalier (Dotclear), Raphaël Goetter et moi-même.
Pour tous les détails, je vous invite à consulter le site du concours: http://mozilla.dotclear.org/.
Faites passer le mot!
Créer un client Twitter offline pour l'iPhone avec HTML5
Par Maurice Svay le jeudi, février 19 2009, 23:30 - Web Tweet
Cet article va expliquer comment créer une application Twitter simple, qui fonctionne qu’on soit connecté ou non. Pour celà, nous allons utiliser des technologies Web, en particulier des nouveautés de HTML5 qui permettent de stocker des données côté client et de mettre des fichiers en cache. Le navigateur Safari de l’iPhone se prête bien à l’exercice, puisqu’il supporte ces technologies.
Les plus pressés peuvent tester immédiatement la démo.
Le squelette de l'application
Tout d’abord, il faut créer le squelette HTML de notre application. Ce fichier HTML devrait faire l’affaire:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; UTF-8" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Language" content="en" />
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=1;"/>
<style type="text/css">
body {
font-family: sans-serif;
margin: 0;
padding: 0;
}
#toolbar {
background: #EFEFEF;
padding: 5px;
border-bottom: 1px solid #a3a3a3;
}
#toolbar button {
border: 1px solid #a1a1a1;
-webkit-border-radius: 3px;
background: #F3F3F3;
padding: 0.5em;
}
ol {
margin: 0; padding: 0;
}
ol li{
list-style: none;
border-bottom: 1px solid #CCC;
margin: 0;
padding: 0.5em;
overflow: auto;
}
span.text {
display: block;
margin-left: 29px;
}
li img {
float: left;
}
.date {
font-size: 0.8em;
color: #AAA;
}
</style>
<title>iPhone Twitter</title>
</head>
<body>
<div id="toolbar">
<button id="load">↻ Refresh</button>
</div>
<ol id="timeline"></ol>
<script type="text/javascript" src="twitter.js"></script>
<script type="text/javascript">
<!--
window.onload = function() {
Twitter.init();
document.getElementById('load').onclick = function() {
Twitter.load();
}
}
-->
</script>
</body>
</html>
Ce fichier se décompose de la manière suivante:
- L’entête HEAD avec le doctype HTML5, une balise META spécifique à l’iPhone, un peu de CSS (et même un peu de CSS3);
- Le corps BODY qui contient une toolbar et une liste ordonnée vide;
- L’inclusion d’un fichier JS et un peu de code pour démarrer l’application et ajouter un le comportement à notre toolbar.
Passons maintenant au fichier twitter.js
Accès à la base de données
La première chose, est d’ouvrir la base de données. Si elle n’existe pas, elle sera créée automatiquement.
var db = null;
try {
if (window.openDatabase) {
db = openDatabase("Twitter", "1.0", "Twitter Feed", 200000);
}
} catch(err) {}
La fonction openDatabase prend 4 paramètres:
- le nom de la base
- la version de la base
- le nom long (ou description)
- la taille attendue de la base
La base sera accessible à travers la variable globale db.
L'objet Twitter
On va ensuite créer un objet Twitter qui va contenir le code pour le chargement et l’affichage des tweets.
var Twitter = {
count: 20
}
Pour le moment il ne contient qu’un attribut de configuration: le nombre de tweets à télécharger à chaque rafraîchissement. Mais nous allons tout de suite lui ajouter des méthodes dans le chapitre suivant.
Création de la table et affichage des tweets
Au chargement de l’application, nous allons tenter de créer la table (si elle n’existe pas) et lire les tweets qu’elle contient pour les afficher. Comme les données sont stockée en local, les tweets s’afficheront, même si la connexion Internet est interrompue.
En revanche, si la base de données n’est pas disponible, nous allons récupérer les tweets directement en ligne.
On va donc ajouter la méthode init qui suit:
Création de la table:
//Create table
init : function() {
console.log('init');
if (db) {
//Have database? Read data
db.transaction(function(tx) {
tx.executeSql(
"CREATE TABLE IF NOT EXISTS status (id REAL UNIQUE, username TEXT, created_at TEXT, text TEXT, avatar TEXT)",
[],
function(result) {
Twitter.readStatus();
},
function(tx, error) {
Twitter.readStatus();
}
);
});
} else {
//No database? Just load data and display
Twitter.load();
}
},
Si la base de données est bien là et la table créée, nous pouvons faire une requête pour récupérer les tweets:
//Read statuses
readStatus : function() {
console.log('read');
db.transaction(function(tx) {
tx.executeSql("SELECT id, username, created_at, text, avatar FROM status ORDER BY id DESC", [], function(tx, result) {
var timeline = document.getElementById('timeline');
timeline.innerHTML = '';
for (var i = 0; i < result.rows.length; ++i) {
var row = result.rows.item(i);
Twitter.display(row);
}
}, function(tx, error) {
// Couldn't retrieve tweets
return;
});
});
},
Rien de très compliqué: la requête de type SELECT récupère les données et on passe chaque ligne résultat à une fonction qui va se charger de les afficher.
L’affichage d’un tweet est effectué par une fonction qui va simplement généré un LI pour l’ajouter à notre liste ordonnée:
//Display statuses
display : function(row) {
console.log('display');
var timeline = document.getElementById('timeline');
var li = document.createElement('LI');
var d = new Date();
d.setTime(Date.parse(row['created_at']));
li.innerHTML =
'<img src="'+row['avatar']+'" alt="" height="24" widht="24"/>'+
'<span class="text">'+
'<strong>'+row['username'] + '</strong> ' +
row['text'] +
' <span class="date">' + prettyDate(d) + '</span>'+
'</span>';
timeline.appendChild(li);
},
Notez que cette fonction utilise une autre fonction prettyDate qui formatte la date pour la rendre lisible. Je ne documenterai pas cette fonction mais vous la trouverez dans la démo.
Remplissage de la base de données
Pour le moment, notre table ne contient aucun tweet, donc rien ne s’affiche. Nous allons charger les données twitter, les stocker et les afficher.
Pour faire simple, nous allons utiliser l’API JSONP de Twitter pour charger les 20 derniers tweets. La méthode load fait plusieurs choses:
- nettoyer les
<script>précédents qui auraient pu être ajouté par un précédent chargement - donnée un feedback visuel pour indiquer que le chargement a démarré
- charger les tweets en insérer le fichier JSONP en tant que
<script>dans le<head> - indiquer que le chargement est terminé lorsque les données sont là
Comme la timeline est protégée par un login et un password, il faudra les entrer lorsque le navigateur le demande.
load : function() {
console.log('load');
//Remove JSONP scripts
var head = document.getElementsByTagName('head')[0];
var scripts = null;
while ((scripts = head.getElementsByTagName('script')).length > 0 ) {
head.removeChild(scripts[0]);
}
document.getElementById('load').innerHTML = 'Refreshing...';
var now = new Date();
var url = 'http://twitter.com/statuses/friends_timeline.json?callback=Twitter.twitterCallback&count={count}&random={random}';
var script = document.createElement('script');
script.setAttribute(
'src',
url.replace('{count}',Twitter.count)
.replace('{random}',now.getTime())
);
script.onload = function() {
document.getElementById('load').innerHTML = '↻ Refresh';
}
console.log(script);
head.appendChild(script);
},
Une fois le chargement terminé, un callback est automatiquement appelé. Si la base de données existe, il va simplement extraire les données qui nous intéressent et les stocker dans la base de données. Sinon, il va afficher chaque tweet.
twitterCallback : function(obj) {
console.log('callback');
if (db) {
var inserts = [];
for (var i=0, l=obj.length; i<l; i++) {
var status = [
obj[i].id,
obj[i].user.screen_name,
obj[i].text,
obj[i].created_at,
obj[i].user.profile_image_url
]
inserts.push(status);
}
Twitter.insert(inserts, Twitter.readStatus);
} else {
//No database? just display
for (var i=0, l=obj.length; i<l; i++) {
Twitter.display({
'id' : obj[i].id,
'username' : obj[i].user.screen_name,
'text' : obj[i].text,
'created_at' : obj[i].created_at,
'avatar' : obj[i].user.profile_image_url
});
}
}
},
Il faut savoir que l‘INSERT est asynchrone et risque de lancer l’affichage avant que nos données soient entièrement stockées. Nous utilisons donc notre propre fonction Twitter.insert qui va faire les insertions et n’afficher qu’à la fin.
Voilà notre méthode d’insertion qui simule une insertion synchrone. Elle prend deux paramètres: le tableau de données et un callback à executer en find d’insertion.
//Synchronous insert
insert : function(arStatus, callback) {
var status = arStatus.pop();
var sql = "INSERT INTO status (id, username, text, created_at, avatar) VALUES (?,?,?,?,?)";
db.transaction(
function (tx) {
tx.executeSql(
sql,
status,
function(tx, result){
if (arStatus.length > 0) {
Twitter.insert(arStatus, callback);
} else {
callback();
}
},
function(tx, error){
if (arStatus.length > 0) {
Twitter.insert(arStatus, callback);
} else {
callback();
}
}
);
}
);
}
Voilà, nous avons tout le code nécessaire pour récupérer des tweets, les stockers localement et les afficher sans repasser par le serveur.
Cache d'applications offline
Jusque là, nous avons créé une application web qui conserve des données en local. Une connexion reste néanmoins nécessaire pour afficher la page HTML et charger le javascript. Heureusement, HTML5 offre un mécanisme qui permet de garder ces deux fichiers en cache et d’y accéder même sans connexion Internet. Cette fonctionnalité est offerte par le cache-manifest, un fichier qui indique au navigateur la liste des fichiers à garder en cache. Ces fichiers ne seront plus jamais rafraichis, à moins que le cache-manifest ne change.
Ce fichier doit être envoyé avec le bon type MIME Content-type: text/cache-manifest (avec PHP par exemple) pour fonctionner. Notez qu’on inclue un commentaire avec la date pour versionner ce fichier. Il suffira de changer la date pour forcer un rechargement complet de l’application.
CACHE MANIFEST #20090201-1731 index.html twitter.js
Un fois ce fichier créé, il suffit de l’inclure dans le fichier HTML en modifiant la balise HTML:
<html manifest="cache-manifest.php">
Conclusion
Si vous avez bien suivi toutes les étapes (et si j’ai bien expliqué), vous devriez avoir une application qui fonctionne totallement offline. Lors de la première connexion, la page HTML et le JS se chargent et sont mis en cache. Ils pourront être accédés n’importe quand par la suite, même sans connexion. L’application permet ensuite d’afficher des tweets stockés localement, sans téléchargement quelque donnée.
Pour télécharger les fichiers complet et tester l’application, tout est disponible à cette adresse: http://svay.com/experiences/iphone-twitter/
Cet exemple est plutôt basique et vous aurez certainement des idées d’applications plus élaborées. D’ailleurs, il semblerait que Google lance dans peu de temps une version offline de GMail pour iPhone en utilisant les même technologies.
Enfin, voilà les liens qui ont servi à préparer ce tuto:
- Offline Web Applications sur le site du W3C
- WebKit Does HTML5 Client-side Database Storage sur le blog de Webkit
Fin
Par mauriz le mardi, octobre 10 2006, 20:19 - Blog Tweet
C'est décidé, ce blog arrive à sa fin. Comme les cool URI don't change, tout restera en ligne. Pas de suppression sauvage. Les commentaires resteront ouverts pendant une semaine encore. Je ne sais pas quoi dire de plus. Ça tombe bien.
« billets précédents - page 2 de 53 - billets suivants »