Introduction
Pour ce tutoriel, nous allons essayer de faire une application de A à Z - enfin presque :
- Prototypage de l'interface utilisateur
- Codage des services
- Intégration et design
Mock-up
Un des trucs rigolos quand on commence une application c'est bien la réalisation de la maquette de l'interface utilisateur. D'abord j'utilise un bon vieux cahier à carreaux (petits les carreaux, c'est mieux pour dessiner), un crayon de bois - ou crayon à papier tout dépend de là où vous habitez, et une bonne gomme. Et on commence à dessiner :

Bon ok, pour le coup j'ai utilisé un Bic - je n'avais pas mon crayon de bois sous la main. Donc c'est bien joli - quoi qu'un peu succinct ;) - mais on va essayer de poser ça sur un ordi tout en gardant le design fait sur un coin de table - pour garder le petit côté artiste de la chose. Et là, bah encore et toujours du web y'a des outils pour tout, alors on en profite - j'utilise iPhone Mockup pour mes applications mobiles et on peut choisir le thème avec une trace de café qui sied si bien. En fonction des clients, on peut choisir un mode plus carré et conventionnel. Pour le premier écran voici l'image résultante : vous pouvez même cliquer dessus et rajouter des trucs - en fait on peut dessiner à plusieurs, c'est plutôt bien fait et du coup les clients peuvent intervenir sur le prototype de l'interface utilisateur :
Edit de dernière minute : il y a maintenant aléatoirement (?) un crayon de bois ou une trace de café :)
La web application
Premier écran
Le premier écran est donc une sélection de points d'intérêts qui pourrait être sympa à afficher sur une carto smartphone. Un clic sur une des catégories renvoie sur la carte et affiche les points correspondants.
Deuxième écran
La carte - centrée en fonction de la géolocalisation de l'utilisateur - avec les marqueurs qui s'affichent. Un clic sur un marqueur ouvre une petite infobulle qui invite à aller sur le troisième écran où seront listés les détails de l'enregistrement cliqué.
Troisième écran
Les détails de l'enregistrement.
Le menu du bas
Il permet de naviguer entre les écrans.
Les données
XAPI MapQuest
La XAPI est une API qui permet de récupérer des données depuis la base de données OSM. Nous avions écris à ce propos un billet sur l'utilisation de celle-ci. Cependant le temps de réponse étant très aléatoire, une utilisation en live est quasiment impossible. Mais il y a quelques semaines MapQuest a ouvert un web service XAPI qui tourne sur leurs serveurs. Et ma foi, là on peut envisager de l'utiliser sur une application :) Merci Mapquest de nous fournir tous ces services.
Les requêtes
La construction d'une requête au service XAPI de Mapquest n'est pas bien compliqué - nous voulons des points dans une certaine zone géographique. Soit :
- des points : node ;
- une catégorie : amenity=key ;
- une zone géographique : BBOX autour de l'utilisateur.
Petit test carto
La réponse fournie par la XAPI étant du XML OSMifié - ça tombe bien :) Et bah OpenLayers peut le lire directement. Le début de l'URL d'appel au service est : http://open.mapquestapi.com/xapi/api/0.6/ . Puis nous posons la question :
- les POI : node
- qui sont des pubs : [amenity=pub]
- à Toulouse : [bbox=1.3053599460914416,43.5462416819936,1.580018149205476,43.6581186827875]
http://open.mapquestapi.com/xapi/api/0.6/node[amenity=pub][bbox=1.3053599460914416,43.5462416819936,1.580018149205476,43.6581186827875]
Vous trouverez ici tous les types d'objets OSM et donc que l'on peut récupérer via la XAPI. Je vous invite à lire les précédents tutos sur MapQuest afin d'avoir le code pour afficher les fonds de carte ; on rajoute donc une couche de données distantes au format OSM :
OpenLayers.ProxyHost = "/cgi-bin/proxy.cgi?url=";
...
var osm_node = new OpenLayers.Layer.Vector("OSM", {
strategies: [new OpenLayers.Strategy.Fixed()],
protocol: new OpenLayers.Protocol.HTTP({
url: "http://open.mapquestapi.com/xapi/api/0.6/node[amenity=pub][bbox=1.3053599460914416,43.5462416819936,1.580018149205476,43.6581186827875]",
format: new OpenLayers.Format.OSM()
}),
projection: new OpenLayers.Projection("EPSG:4326")
}
);
map.addLayer(osm_node);
Préalablement, il a fallu éditer le fichier proxy.cgi et ajouter l'url de la XAPI.
Jusque là rien de trop nouveau, y'a pas mal de bars place Saint-Pierre ... mais pour ceux qui connaissent un peu le coin, rien d'extraordinaire à cela. Et on avait déjà affiché des données provenant de la XAPI, pas celle de MapQuest mais le principe est le même.
Les contraintes - les données et le framework
Tout d'abord
Pour l'application que nous voulons développer, il faudrait rendre la carto affichable dans un smartphone (ça OpenLayers le fait très bien depuis le code sprint de ce printemps), pouvoir jouer sur la BBOX (ie. soit la position de l'utilisateur si il autorise que le site web récupère sa géolocalisation, soit les coordonnées d'une recherche qu'il effectue) et pouvoir modifier le type d'objets à afficher (changer [amenity=pub] en autre chose).
Quelques limitations de la XAPI
La XAPI est limitée par la taille de l'emprise géographique : 10 degrés carrés. Il faudrait donc théoriquement recharger les données chaque fois que l'on s'éloigne de cette emprise. Cependant, l'application est mobile par essence (exemple : "y-a-t'il un pub musée pas trop loin d'où je me trouve ?") - nous n'afficherons les données que pour un niveau de zoom assez élevé ; un bouton de rechargement des données devrait suffire si l'utilisateur s'amuse à se déplacer assez loin de son point d'origine.
Quelle bibliothèque ?
Maintenant que nous avons à peu près toutes les billes pour commencer à intégrer les éléments dans l'application, il faut choisir le framework qui va nous aider - bon en fait normalement on le choisit un peu plus en amont du projet ou alors il est imposé et faut faire avec. Nous utiliserons ici jQuery Mobile parce que voilà c'est comme ça :) - et puis on a déjà utilisé Sencha Touch donc on change et aussi parce que dans les exemples d'OpenLayers, l'intégration de la carto avec ce framework a déjà été testée - on ne va pas partir de rien.
Développement
Préparation des pages
Avant toutes choses compliquées, initialisons nos trois vues avec jQuery Mobile le tout dans une même page HTML. Nos vues seront composées d'une en-tête 'header', d'un contenu 'content' et d'un pied de page persistant navigable 'footer'. La structure :
GeoTribu - POI Mobile
...
...
...
Puis initialisons les trois vues - tout d'abord la carte :
Map
Puis la liste des POI :
Et enfin la vue des détails des POI sélectionnés sur la carte :
Normalement vous devriez avoir une webappli de ce style :
Ca commence à prendre forme :)
Ajoutons OpenLayers dans le bloc 'map'
Le script d'OpenLayers utilisé est celui de dev, il supporte les fonctions mobiles : il est disponible à cette adresse : http://openlayers.org/dev/OpenLayers.js. Dans l'ordre il va falloir :
- créer deux scripts - un pour OpenLayers et la déclaration de la carte, et un pour jQuery afin de piloter la taille de la carte en fonction du device ;
- modifier un peu la feuille de style pour définir la taille de la carte et ajouter les boutons de zoom (OpenLayers sur Android ne supporte pas encore le zoom en pinçant ... en tout cas j'ai pas réussi).
Créons un fichier mobile-base.js pour OpenLayers et appelons-le depuis la page HTML :
Notons que nous appelons également le script dev OpenLayers. Déclarons la carte et la projection spherical mercator dont nous aurons besoin puis une fonction init qui sera appelée judicieusement par le script jQuery :
var map;
var mercator = new OpenLayers.Projection("EPSG:900913");
var mapquest;
var init = function(){
...
}
Et ajoutons notre carte MapQuest dans cette fonction init() :
mapquest = new OpenLayers.Layer.OSM("MapQuest", "http://otile1.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png", {'sphericalMercator': true, isBaseLayer:true});
map = new OpenLayers.Map({
div: "map",
theme: null,
projection: mercator,
units: "m",
numZoomLevels: 18,
maxResolution: 156543.0339,
maxExtent: new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34),
controls: [
new OpenLayers.Control.Attribution(),
new OpenLayers.Control.TouchNavigation({
dragPanOptions: {
interval: 100,
enableKinetic: true
}
})
],
layers: [
mapquest
],
center: new OpenLayers.LonLat(0, 0),
zoom: 1
});
Rien de bien nouveau ici si ce n'est le contrôle TouchNavigation pour la navigation avec les doigts.
Initialisation avec jQuery
Nous y sommes presque : il reste à initialiser l'application et adapter la carte à la taille de l'écran du device. On va faire cela avec jQuery. Première chose rediriger l'utilisateur vers le bloc 'mappage' afin que l'application s'ouvre par défaut sur la carte :
$(document).ready(function() {
if (window.location.hash && window.location.hash!='#mappage') {
$.mobile.changePage('mappage');
}
...
});
La carte OpenLayers n'ayant pour le moment aucune taille, il faut la dimensionner en fonction du smartphone sans toucher au contenu des deux autres pages - on va donc manipuler dans une fonction le bloc 'contentmap' en ajustant la taille de la carte OpenLayers à la taille de l'écran moins le footer de navigation - on appelle cette fonction chaque fois que l'orientation du mobile change ou que la taille de l'écran change avec la fonction bind() de jQuery :
$(document).ready(function() {
if (window.location.hash && window.location.hash!='#mappage') {
$.mobile.changePage('mappage');
}
function fixContentHeight() {
var footer = $("div[data-role='footer']");
content = $("div[data-role='contentmap']");
viewHeight = $(window).height();
contentHeight = viewHeight - footer.outerHeight();
if ((content.outerHeight() + footer.outerHeight()) !== viewHeight) {
contentHeight -= (content.outerHeight() - content.height());
content.height(contentHeight);
}
if (window.map) {
map.updateSize();
map.zoomIn();
map.zoomOut();
} else {
init(function() {});
}
}
$(window).bind("orientationchange resize pageshow", fixContentHeight);
Initialisation des feuilles de style
Si vous testez maintenant, la carte risque de ne pas s'afficher correctement, il va falloir ajouter un peu d'éléments de style. Créons donc deux fichiers : un pour OpenLayers et un pour jQuery et appelons-les depuis la page index.html :
Tout d'abord celui dans la feuille pour OpenLayers, il faut spécifier le bloc relatif au copyright :
.olControlAttribution {
font-size: 10px;
bottom: 5px;
right: 5px;
}
Puis dans celui pour jQuery les blocs html, body, map, content et footer :
html {
height: 100%;
}
body {
margin: 0;
padding: 0;
height: 100%;
}
.ui-content {
padding: 0;
}
.ui-footer {
text-align: center;
}
#map {
width: 100%;
height: 100%;
}
Ajoutons des boutons de zoom
Sur Android et donc Chrome Mobile, le zoom en pinçant l'écran ne fonctionne pas (en tout cas j'ai pas réussi ... bizarre) à la différence de l'iPhone et Safari. Ajoutons donc deux boutons (déclarés comme des éléments jQuery) dans le bloc mappage, une bonne pelletée de CSS pour ces derniers et des évènements lorsque ceux-ci sont cliqués :
Dans la feuille de style jQuery :
#navigation {
position: absolute;
bottom: 40px;
left: 10px;
z-index: 1000;
}
#navigation .ui-btn-icon-notext {
display: block;
padding: 7px 6px 7px 8px;
}
.ui-content .ui-listview {
margin: 0px;
}
Dans la feuille de style OpenLayers :
div.olControlZoomPanel {
height: 108px
width: 36px;
position: absolute;
top: 20px;
left: 20px;
}
div.olControlZoomPanel div {
width: 36px;
height: 36px;
}
div.olControlZoomPanel .olControlZoomInItemInactive, div.olControlZoomPanel .olControlZoomOutItemInactive {
background: rgba(0,0,0,0.2);
position: absolute;
}
div.olControlZoomPanel .olControlZoomInItemInactive {
border-radius: 5px 5px 0 0;
background-position: 0 0;
}
div.olControlZoomPanel .olControlZoomOutItemInactive {
border-radius: 0 0 5px 5px ;
top: 37px;
background-position: 0 -72px;
}
div.olControlZoomPanel .olControlZoomInItemInactive:after{
content: '+';
font-size: 36px;
padding-left: 7px;
z-index: 2000;
color: #fff;
line-height: 0.9em;
}
div.olControlZoomPanel .olControlZoomOutItemInactive:after{
content: '–';
font-size: 36px;
padding-left: 7px;
z-index: 2000;
color: #fff;
line-height: 0.9em;
}
div.olControlZoomPanel .olControlZoomToMaxExtentItemInactive {
display: none;
top: 36px;
background-position: 0 -36px;
}
Et les évènements :
$("#plus").click(function(){
map.zoomIn();
$("#plus").removeClass("ui-btn-active");
$("#plus").addClass("ui-btn-inactive");
});
$("#minus").click(function(){
map.zoomOut();
$("#minus").removeClass("ui-btn-active");
$("#minus").addClass("ui-btn-inactive");
});
Ajoutons la géolocalisation GPS
HTML5 permet la géolocalisation via le GPS du smartphone si celui-ci en est équipé et que l'utilisateur veut bien 'partager' (je préfère le terme 'utiliser' dans notre cas, en effet je n'envoie rien au serveur). Nous utiliserons la classe Geolocate d'OpenLayers. Ajoutons tout d'abord notre bouton sur la carte (donc dans le bloc 'mappage') dans le HTML et dans la feuille de style jQuery :
$("#buttonlocate").click(function(){
...
$("#buttonlocate").removeClass("ui-btn-active");
$("#buttonlocate").addClass("ui-btn-inactive");
});
Dans le script OpenLayers ajoutons le contrôle Geolocate, deux styles pour représenter la position (une petite croix rouge et un cercle) et un évènement sur celui-ci :
var geolocate;
var vector;
var locatecontrol;
var init = function () {
geolocate = new OpenLayers.Control.Geolocate({
id: 'locate-control',
geolocationOptions: {
enableHighAccuracy: false,
maximumAge: 0,
timeout: 7000
}
});
map = new OpenLayers.Map({
...,
controls: [...,
geolocate
]...
});
var style = {
fillOpacity: 0.1,
fillColor: '#000',
strokeColor: '#f00',
strokeOpacity: 0.6
};
var stylecross = {
graphicName: 'cross',
strokeColor: '#f00',
strokeWidth: 2,
fillOpacity: 0,
pointRadius: 10
};
geolocate.events.register("locationupdated", this, function(e) {
vector.removeAllFeatures();
vector.addFeatures([
new OpenLayers.Feature.Vector(
e.point,
{},
stylecross
),
new OpenLayers.Feature.Vector(
OpenLayers.Geometry.Polygon.createRegularPolygon(
new OpenLayers.Geometry.Point(e.point.x, e.point.y),
e.position.coords.accuracy / 2,
50,
0
),
{},
style
)
]);
map.zoomTo(13);
});
locatecontrol = map.getControlsBy("id", "locate-control")[0];
Et enfin l'activation de cet évènement dans jQuery chaque fois que l'on clique sur le bouton :
$("#buttonlocate").click(function(){
if (locatecontrol.active) {
locatecontrol.getCurrentLocation();
} else {
locatecontrol.activate();
}
$("#buttonlocate").removeClass("ui-btn-active");
$("#buttonlocate").addClass("ui-btn-inactive");
});
Ca avance sérieusement notre web application :)
Liaison de la liste de POI avec la carte
Maintenant que nous avons la carte, il nous reste encore un peu de boulot : lors d'un clic sur une catégorie dans la première page, aller sur la carte et afficher les POI. Ca devrait pouvoir se faire en posant les bonnes questions et en codant une petite fonction.
Dans notre script OpenLayers ajoutons donc une fonction qui a pour but de faire une requête vers la XAPI de MapQuest puis d'afficher le résultat sur la carte :
var osm_node;
var url_xapi_mapquest;
var gmprojection = new OpenLayers.Projection("EPSG:4326");
var init = function(){
OpenLayers.ProxyHost = "/cgi-bin/proxy.cgi?url=";
...
}
function addOSMNode(amenity){
if (map.getZoom() >= 13){
if (osm_node){
osm_node.destroy();
}
var zoomTemp = map.getZoom();
map.zoomTo(13)
bbox = map.calculateBounds().transform(mercator, gmprojection).toBBOX();
url_xapi_mapquest = "http://open.mapquestapi.com/xapi/api/0.6/node[amenity="+amenity+"][bbox="+bbox+"]";
osm_node = new OpenLayers.Layer.Vector("OSM", {
strategies: [new OpenLayers.Strategy.Fixed()],
protocol: new OpenLayers.Protocol.HTTP({
url: url_xapi_mapquest,
format: new OpenLayers.Format.OSM()
}),
projection: gmprojection
}
);
map.addLayer(osm_node);
} else{
alert('Bounding box trop grande, veuillez zoomer un peu :)');
}
}
Donc dans l'ordre nous avons fait :
- si la niveau de zoom est trop faible nous invitons l'internaute à zoomer un peu - il faut garder à l'esprit que la zone de requête de la XAPI ne peut dépasser 10 degrés carré ;
- si des POI sont déjà affichés, nous les supprimons ;
- nous sauvegardons le niveau de zoom actuel ;
- nous calculons la bounding box en simulant un niveau de zoom 13 ;
- nous créons l'url de requête à la XAPI en passant le niveau de zoom et le type de catégorie de POI passé en paramètre dans la fonction ;
- nous revenons au niveau de zoom précédent ;
- et nous affichons les POI sur la carte.
Appelons maintenant cette fonction via l'évènement onclick depuis la liste de POI en ayant ajouté préalablement un identifiant à chacune des catégories qui sera le paramètre de la fonction :
Ajoutons quelques couches OpenStreetMap et un popup de sélection
Il serait dommage de se limiter qu'à une seule couche de fond de carte - y'en a pas mal basées sur OSM - profitons-en. Evidemment nous pourrions rajouter les couches Google, Bing et Yahoo. Donc rajoutons un petit bouton sur la carte qui ouvre une popup de sélection des fonds de carte :
Baselayer
Choose the baselayer
Bon là je vous avouerai que j'ai spécialement galéré pour essayer de garder persistant le style de la liste à l'intérieur d'une popup (ie. data-role="dialog" en jQuery) : c'est pour cette raison que j'ai rajouté une balise font et du css pour les éléments de la liste ...
... hum en fait ça marche pas ni sous Safari iPhone ni sous Chrome Android ... Espérons que les navigateurs sous mobile se mettent d'accord afin que les bibliothèques soient performantes sur ces derniers.
#navigationbaselayer {
position: absolute;
bottom: 40px;
right: 10px;
z-index: 1000;
}
#navigationbaselayer .ui-btn-icon-notext {
display: block;
padding: 7px 6px 7px 8px;
}
#base p {
margin: 5px;
}
.baselist{
background: -moz-linear-gradient(center top , #FEFEFE, #EEEEEE) repeat scroll 0 0 #EEEEEE;
text-shadow: none;
}
Et donc la fonction de sélection du fond de carte dans le script OpenLayers :
var mapnik;
var osmarender;
var init = function () {
...
osmarender = new OpenLayers.Layer.OSM("Osmarender", "http://tah.openstreetmap.org/Tiles/tile/${z}/${x}/${y}.png", {'sphericalMercator': true, isBaseLayer:true});
mapnik = new OpenLayers.Layer.OSM("Mapnik", "http://tile.openstreetmap.org/${z}/${x}/${y}.png", {'sphericalMercator': true, isBaseLayer:true});
map = new OpenLayers.Map({
...
,
layers: [
mapquest,
osmarender,
mapnik,
vector
]
...
});
...
};
function setBase(layer){
var baselayer = map.getLayersByName(layer);
map.setBaseLayer(baselayer[0]);
}
Personnalisons les icônes des marqueurs
Il faut dorénavant personnaliser un peu les icônes qu'elles aient la même tête que les vignettes dans la liste de POI, histoire d'avoir une cohérence graphique.
Nous allons seulement ajouter un style qui ira chercher la bonne icône (une réduction de celle présente dans la liste) :
osm_node = new OpenLayers.Layer.Vector("OSM", {
styleMap: new OpenLayers.StyleMap({
externalGraphic: "resources/img/gmaps_"+amenity+".png",
graphicOpacity: 1.0,
graphicWith: 16,
graphicHeight: 27,
graphicYOffset: -27
}),
...
});
Les détails des POI
Il nous reste à lier les marqueurs avec la page de détails : ajoutons un évènement qui lors d'un clic sur un marqueur affiche la page 'détails' mises à jour des informations que l'on possède via la XAPI sur ce POI.
L'évènement est à proprement parler un contrôle fourni par OpenLayers qu'il suffit de lier à la carte et aux éléments sélectionnés :
function addOSMNode(amenity){
...
map.addLayer(osm_node);
var selectControl = new OpenLayers.Control.SelectFeature(osm_node, {
autoActivate:true,
onSelect: setDetails
});
map.addControl(selectControl);
...
}
Et d'ajouter une fonction qui 'ouvre' la page de détails et qui remplit le contenu avec les attributs fournis par la XAPI :
function setDetails(feature){
$.mobile.changePage('details');
var html = '';
var amenity_name = '';
for (var key in feature.attributes) {
if (key == 'amenity'){
$("#detailsheader").html(feature.attributes[key].toUpperCase());
} else if (key == 'name'){
amenity_name = "Name : "+feature.attributes[key]+"
";
} else {
html += key+' : '+feature.attributes[key]+"
";
}
}
if (amenity_name != ''){
html = amenity_name+'
'+html;
}
var about = "";
about += "Made by team GeoTribu";
about += "
Data by OpenStreetMap";
about += "
XAPI by MapQuest";
about += "
Licence : CC BY-SA 2.0
";
about += "";
html += '
'+about;
$("#detailscontent").html(html);
}
Dans l'ordre :
- on change de page ;
- on parcourt les attributs de l'élément passé en paramètre ;
- on modifie le contenu de la page de détail ainsi que le titre ;
- on ajoute les copyrights.
C'est bientôt fini, une petite modification de la page HTML et un peu de CSS :
Details
Details of POI tap on map.
...
div#detailscontent {
margin: 20px;
}
div#about {
border-style: dashed;
}
div#about p {
margin: 10px;
}
Finalisation
Icône pour la page d'accueil
L'utilisation d'une web application est souvent plus agréable en pleine page, c'est pourquoi nous allons proposer à l'utilisateur d'ajouter un raccourci sur son bureau avec une belle icône - sur iPhone ça a l'avantage de pouvoir naviguer sans les barres d'url de Safari.
Pour ajouter une icône personnalisée il suffit de créer une image de 57 pixels par 57 pixels et de l'appeler de cette manière - elle doit porter ce nom : iphone_icon.png (pour Android je n'ai pas trouvé ...).

Flashouillez le code ci-dessus pour aller directement sur l'application avec votre smartphone ou alors rdv à cette adresse - http://geotribu.net/applications/geotribupoi/index.html ou celle-ci http://goo.gl/EkXN9 pour le mode plus court.
Pour aller plus loin : le localStorage
Voilà nous avons notre application mais la liste des POI est fixée par le webmaster, c'est bien mais autant donner la main à l'utilisateur. Pour cela il serait possible grâce au localStorage de sauvegarder ses préférences en donnant la possibilité de choisir les POI qu'on veut afficher.
En simplifiant la fonctionnalité localStorage, c'est comme un super cookie.
Pour développer une telle fonctionnalité, il faudrait mettre en place sur le serveur une page référençant un grand nombre de catégories de POI possible et ajouter les icônes correspondantes. Enfin il faudrait mettre un bouton dans la liste afin d'ajouter des catégories et de donner la possibilité à l'utilisateur de pouvoir modifier ses choix.
Ce sera pour plus tard ;)
Conclusion
A n'en pas douter à l'avenir il faudra bien faire la part des choses et bien choisir entre développer une application (Android SDK et Java, XCode, ...) et une web application. Dans un cas relativement simple comme celui-ci une web application à l'avantage d'être potentiellement déployée sur tous types de smartphones avec un seul et unique développement. Espérons que bientôt les navigateurs des smartphones suivront les mêmes progrès que font vos butineurs usuels afin de suivre des spécifications communes.
Cette web application est un proof of concept de l'utilisation conjointe d'OpenLayers, d'OpenStreetMap et de jQuery - évidemment elle ne répond pas encore pareil sur les iPhone et sur les Android (quid des WindowsPhone ???). J'ai testé avec un iPhone 3GS et un Nexus S, le résultat est beaucoup plus agréable sur iPhone - pour le moment c'est incomparable (la différence viendrait-elle de jQuery Mobile ?).
J'espère n'avoir pas été trop long mais j'ai voulu expliquer comment je voyais la 'fabrication' d'une web application en partant d'un papier sur un coin de table jusqu'au site web développé.
Auteur : Fabien - fabien.goblet [ at ] gmail.comCommentaires
Jay replied on Permalien
boulou replied on Permalien

