[Application 57] Ajout d’un Back End noSql Firebase (Application Partagée.)

firebase.jpgIntroduction


Vu que l’exercice était sympa, je décide de transformer l’application en ajoutant un Back end et plusieurs utilisateurs. Pour le tester, cliquer ici .

Attention ce n’est compatible qu’avec Firefox, il faudra un serveur Https Pour que cela fonctionne avec les autres navigateurs.

Jour 1 : 5h30 de code

Explication du Code


A la base, sur le téléphone mobile, les photos sont stockées en base64, mais pour les stocker sur le back-end, il est moins couteux de stocker les urls des images, chaque image d’un coin est donc convertie en Binaire dans une boucle for, puis envoyée sur le serveur Back end . Là, un script PHP très simple crée les fichiers images, puis les stocke à un endroit défini. Les url  des images sont, elles, stockées dans l’objet json $scope.nouvelEndroit, qui est à son tour enregistré en Back end sur Firebase.

Problèmes rencontrés (provisoires ?):


La vue principale ne passe absolument pas dans le routeur ngroute, je ne sais pas pourquoi, j’ai du la conserver dans index.html et « bricoler » avec Jquery avec ngHide et ngshow, je deteste quand ça fait cela.  La deuxième vue, la liste, pose également problème au niveau du tri, j’ai du également la laisser dans index.html, je deteste cela. Du coup ngroute permet uniquement de switcher de controleur quand on change de vue.
Il faudrait faire du lazy loading je pense, j’essayerais demain c’est très énervant !

$scope.endroits est une liaison tridirectionnelle à Firebase, en temps réelle, qui se mets à jour sur tous les ordinateurs et mobiles connectés.

Exemple d’un enregistrement sur Firebase :


firebase2

On voit que je stocke les urls des photos, et pas le format base64, qui est très couteux en accès réseau !

Le code :


Attention, c’est un peu hard, la boucle pour convertir les images n’est pas très chaude à faire mais à lire … hum !

Les deux controleurs AngularJs :

angular.module('neutre', ['ui.knob','firebase','ngRoute'])

  <!-- Configuration des vues et de leurs controleurs affiliés, avec ng-route . Note : Les vues peuvent partager le même controleur-->
        .config(function ($routeProvider, $locationProvider){
          $routeProvider
           .when('/accueil', {

              controller: 'neutreCtrl'
            })
            .when('/liste', {
              templateUrl: 'liste.html',
              controller: 'listeCtrl'
            })
            .otherwise({redirectTo: '/accueil'});
        })

.controller('neutreCtrl', function($scope,$http,$timeout,$firebaseArray,$firebaseAuth,$firebaseObject,$route) {

	/* INITIALISATION DES VARIABLES ET DES OBJETS*/
	$( "#liste" ).hide();
	var d = new Date();
	var d = d.toString();
	photos = []; // Contient les photos en BASE64 d une nouvelle evaluation prise avec un telephone portable
	$scope.nouvelEndroit = [];
	$scope.nouvelEndroit.date = d;
	$scope.nouvelEndroit.photos = []; // Ici seront stockées les noms classiques en .jpeg des photos une fois qu on les a converti de base64 a binaire
	$scope.endroits = []; // Liste des endroits visitéss
	var ref = new Firebase("https://blinding-heat-8502.firebaseio.com/endroits/");
	$scope.endroits = $firebaseArray(ref); // $scope.endroits est en liaison tri-directionnelle permanente avec le back end firebase, il contient tous les endroits visités et se mets à jour automatiquement sur tous les ordinateurs ou mobiles connectés.
	/* CRUD  */

	/* Sauvegarde une évaluation, avec les photos en base 64 la date, ladresse est acquise par google map */
	$scope.enregistrerEvaluation = function(){

		/* CONVERSION DES PHOTOS DE BASE 64 A BINAIRE PUIS ENVOI SUR LE SERVEUR QUI STOCKE LES FICHIERS AVEC PHP SUR LE SERVEUR*/
		var x = 0;
		for (x=0;x<photos.length;x++){
			var blob = dataURItoBlob(photos[x]);
			var file = new File([blob], ''+$scope.nouvelEndroit.nom+'-'+x+'.jpeg', {type: "'image/jpeg"}); // Génération dun nom pour chaque photo
			envoiPhotoSurServeur(file); // Envoi sur le serveur back end de chaque photo qui se transforme en fichier
			$scope.nouvelEndroit.photos.push(''+$scope.nouvelEndroit.nom+'-'+x+'.jpeg'); // stockage des urls des photos dans l objet json nouvelEndroit pour pouvoir les retrouver plus tard
		}
		$scope.endroits.$add($scope.nouvelEndroit); /* Enregistre en Back End sur Firebase. */
		initEndroit(); // On passe à un autre endroit, donc on initialise le formulaire
	}

	/* ENVOI DE FICHIERS BINAIRES SUR LE SERVEUR */
	function envoiPhotoSurServeur(file){
		var fd = new FormData();
		fd.append("file", file);
		console.log(file.name);
		var uploadUrl = "uploadPhotos.php";
				$http.post(uploadUrl, fd, {
					withCredentials: true,
					headers: {'Content-Type': undefined },
					transformRequest: angular.identity
				}).success(function(data){
					$scope.infoUpload="Fichier téléchargé sur le serveur ";
					})
					.error(function(data){
						$scope.infoUpload="Echec du téléchargement sur le serveur";
					});
	};

	/* CONVERTISSEMENT BASE64->BLOB . ON PASSE DU FORMAT BASE64 AU FORMAT BINAIRE POUR ENSUITE L ENVOYER AU BACK END */
	function dataURItoBlob(dataURI) {

		// convert base64/URLEncoded data component to raw binary data held in a string
		var byteString;
		if (dataURI.split(',')[0].indexOf('base64') >= 0)
			byteString = atob(dataURI.split(',')[1]);
		else
			byteString = unescape(dataURI.split(',')[1]);

		// separate out the mime component
		var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

		// write the bytes of the string to a typed array
		var ia = new Uint8Array(byteString.length);
		for (var i = 0; i < byteString.length; i++) {
			ia[i] = byteString.charCodeAt(i);
		}

		return new Blob([ia], {type:mimeString});
	}

	/* REINITIALISATION DU FORMULAIRE APRES UNE EVAL */
	initEndroit = function(){
		var d = new Date();
		photos = [];
		$scope.nouvelEndroit = [];
		$scope.nouvelEndroit.date = d;
		$scope.nouvelEndroit.photos = [];
		$('#listePhoto').html("");

		getAdresse($scope.latitude,$scope.longitude);

	}

	/* GEOLOCALISATION */
	// Detecte l'api avant de l'utiliser 

	if (navigator.geolocation) {
	 // Insère la carte dans la div "map"
		var mapElem = document.getElementById("map"),

		// cette fonction en cas de succès dacces à la position récupère la long et la lat puis affiche google map

			successCallback = function(position) {
				var lat = position.coords.latitude,
					long = position.coords.longitude;
			$scope.$apply(function(){
				$scope.latitude 	= 	lat;
				$scope.longitude	=	long;
				getAdresse(lat,long);
			});

				mapElem.innerHTML = '<img src="http://maps.googleapis.com/maps/api/staticmap?markers=' + lat + ',' + long + '&zoom=15&size=300x300&sensor=false" />';

			},

		// Cette fonction se lance si l'endroit ne peut pas être localisé
		errorCallback = function() {
				  alert("Désolé, je ne peux pas trouver vos coordonnées GPS, êtes vous sous Firefox ?");
			};

		// Start watching the user’s location, updating once per second (1s = 1000ms)
		// and execute the appropriate callback function based on whether the user
		// was successfully located or not

		navigator.geolocation.watchPosition(successCallback, errorCallback, {
			maximumAge: 1000
		});
	}

	/* RETROUVE L ADRESSE EN DONNANT LA LATITUDE ET LA LONGITUDE */
	getAdresse = function (lat,long){
		$http.get('http://maps.googleapis.com/maps/api/geocode/json?latlng=' + lat + ',' + long + '&sensor=true',{header : {'Content-Type' : 'application/json; charset=UTF-8'}}).success(function(data){
			/* console.log(data.results[0].formatted_address); */
			/* POSITION SE METS A JOUR EN TEMPS REEL AVEC LE $SCOPE */
			$scope.adresse = data.results[0].formatted_address;
			$scope.nouvelEndroit.adresse = data.results[0].formatted_address;
			}).error(function(data){ $scope.infos = " Pas de données ou pb de connexion"});

	}

	/* GESTION DE LA VIDEO ET DE LA PRISE DE PHOTOS*/

	  ;(function(){
		/*la fonction générique qui est censée s adapter a tous les navigateurs */
        function userMedia(){
            return navigator.getUserMedia = navigator.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia ||
            navigator.msGetUserMedia || null;

        }

        // Maintenant, on peut l utiliser
        if( userMedia() ){
            var videoPlaying = false;
            var constraints = {
                video: true,
                audio:false
            };
            var video = document.getElementById('v');

            var media = navigator.getUserMedia(constraints, function(stream){

                // L url de l objet est different dans webkit
                var url = window.URL || window.webkitURL;

                // creer l url et set la source de la video
                video.src = url ? url.createObjectURL(stream) : stream;

                // lance la video
                video.play();
                videoPlaying  = true;
            }, function(error){
                console.log("ERROR");
                console.log(error);
            });

            // Ecoute l action utilisateur sur le bouton prendre une photo
            document.getElementById('prends').addEventListener('click', function(){
                if (videoPlaying){
                    var canvas = document.getElementById('canvas');
                    canvas.width = video.videoWidth;
                    canvas.height = video.videoHeight;
                    canvas.getContext('2d').drawImage(video, 0, 0);
                    var data = canvas.toDataURL('image/webp');

					photos.push(data); /* Ajoute la nouvelle photo au tableau de photo en temps réel */

					/* MISE A JOUR EN JQUERY DE LA LISTE DES PHOTOS. AOUT DE LA NOUVELLE PHOTO QUI VIENT DETRE PRISE */
					$('#listePhoto').prepend('<img  src="' + data + '" style="width:150px;"/>')// Affiche avec Jqeury la photo en base 64 dans la liste des photos
				}
            }, false);

        } else {
            console.log("KO");
        }
    })();

	/* Parametrage de l'aspect des KNOBS */
		$scope.options = {
		  skin: {
			type: 'tron'
		  },
		  size: 150,
		  unit: "",
		  barWidth: 20,
		   bgColor: '#2C3E50',
			 barColor: '#FFAE1A',
		  textColor: '#eee',
		  trackColor: 'rgba(255,0,0,.1)',
		  prevBarColor: 'rgba(0,0,0,.2)',
		  subText: {
			enabled: false,
			text: 'CPU used'
		  },
		  scale: {
			enabled: true,
			type: 'lines',
			width: 3
		  },
		  step: 0.1,
		  max:10,
		  displayPrevious: true
		};

		/* Montrer un message Bootstrap sur click. */
		 $scope.alertMsg = {
				show: false,
				text:"Alert message is ::show"
			};
			$scope.showAlert =function(){
				 $scope.alertMsg.show = true;
				$timeout(function(){
					$scope.alertMsg.show = false;
				}, 2000);
			}

/* FIN DU CONTROLEUR */
})

 .controller('listeCtrl', function($scope,$http,$timeout,$firebaseArray,$firebaseAuth,$firebaseObject,$route) {
	 $( "#evaluation" ).hide();
	 $( "#liste" ).show();
	 var ref = new Firebase("https://blinding-heat-8502.firebaseio.com/endroits/");
	$scope.endroits = $firebaseArray(ref); // $scope.endroits est en liaison tri-directionnelle permanente avec le back end firebase, il contient tous les endroits visités et se mets à jour automatiquement sur tous les ordinateurs ou mobiles connectés.
})

  

Le fichier HTML qui contient les deux vues, qui ne fonctionnent pas avec le controleur.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Evalue ton coin</title>
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
		<!-- CHARGEMENT DES LIBRAIRIES -->

		<!-- JQUERY ET BOOTSRAP -->
        <script src="bower_components/jquery/dist/jquery.min.js"></script>
        <script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
        <link rel="stylesheet" type="text/css" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
		<link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css" type="text/css">

        <!-- ANGULARJS -->
        <script src='bower_components/angular/angular.min.js'></script>
		 <script src="bower_components/angular-route/angular-route.min.js"></script>
        <script src="bower_components/angular-locale_fr-fr/angular-locale_fr-fr.js"></script>

		<!-- NG -KNOB -->
         <script src="bower_components/d3/d3.min.js"></script>
        <script src="bower_components/ng-knob/dist/ng-knob.min.js"></script>

		 <!-- CHARGEMENT LIB FIREBASE -->
		<script src='https://cdn.firebase.com/js/client/2.2.1/firebase.js'></script>

		<!-- MODULE ANGULARJS AngularFire pour firebase-->
		<script src="https://cdn.firebase.com/libs/angularfire/1.1.3/angularfire.min.js"></script>

		<!-- APPLICATION PERSO -->
		<script src="js/app.js"></script>
		<link rel="stylesheet" type="text/css" href="css/style.css">

		<!-- FIN DE CHARGEMENT DES LIBRAIRIES -->
  </head>

<body ng-app="neutre" ng-controller="neutreCtrl">
	<div class="container-fluid">

        <nav class="navbar navbar-inverse">
            <div class="container-fluid">
                <!-- Brand and toggle get grouped for better mobile display -->
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                        <span class="sr-only">Toggle navigation</span>
                    </button>
                    <a class="navbar-brand" href="#">Evalue ton coin</a>
					 <!-- Le menu, qui appelle,grâce à HREF, les différentes vues HTML situées plus bas, puis les affiche dans la balise ng-view -->
						<div class="container ">
							<div class="menu designDiv">
								<a href="#/accueil" onclick="$('#evaluation').show();$( '#liste' ).hide();" class="">Accueil</a>
								<a href="#/liste" class="">Liste  </a>
							</div>

						</div>
                </div>
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
        </nav>

		<!-- Cette balise NG-View va afficher les différentes vues -->

								<div ng-view></div>

	<div class="row" id="evaluation">
		<div class="col-lg-12 " >
			 <div class="panel panel panel-warning">

				<div class="panel-heading">
					<i class="fa fa-comment-o"></i> Evalue le coin :
					<div class="box-tools pull-right">
						<button class="btn btn-primary btn-sm pull-right" ng-model="collapsed" ng-click="collapsed=!collapsed" data-widget="collapse"><i class="fa fa-minus"></i></button>
					</div>
				</div>

				<div ng-show="!collapsed" class="panel-body" style="min-height:350px;">

					<div class="col-lg-6 " >
						<div class="panel  panel-primary ">
							 <div class="panel-heading" >
								<i class="fa fa-location-arrow"></i> Ta position
							</div>
							<div class="panel-body">
								<div><b>Longitude :</b> {{longitude}} Latitude : {{latitude}}</div>
								<div><b>Adresse :</b>{{adresse}} </div>
								<div style="overflow:auto"><div id="map" ></div></div>
							</div>
						</div>
					</div>

					<div class="col-lg-6 " >
						<div class="panel  panel-primary ">
							 <div class="panel-heading" >
								<i class="fa fa-camera"></i> Prends ta photo
							</div>
							<div class="panel-body text-right">
								<video id="v" style="width:100%;"></video>
								<button id="prends" class="btn-warning btn-block"><i class="fa fa-camera"></i> Prends une photo</button>
							</div>
						</div>
					</div>

					<div class="col-lg-12 " >
						<div class="panel  panel-primary ">
							 <div class="panel-heading" >
								<i class="fa fa-picture-o"></i> Liste de tes photos
							</div> <canvas id="canvas" style="display:none;width:50px;"></canvas>
							<div class="panel-body" id="listePhoto" style="min-height:150px;">

								<!-- <img src="" id="photo" alt="photo">   -->
								<!-- <div ng-repeat="photo in photos"><img ng-src="{{::photo}}" style="width:150px;float:left"></img></div>  -->
							</div>
						</div>
					</div>

					<div class="col-lg-12 " >
						<div class="panel  panel-danger  ">
							 <div class="panel-heading" >
								<i class="fa fa-area-chart"></i> Affecte une note
							</div>
								<div class="panel-body" style="overflow:auto" >
									<div class="form-group input-group-sm col-lg-3" style="float: left;">
										<span class="input-group-addon " ><i class="fa fa-university"></i> Propreté</span>
										<ui-knob value="nouvelEndroit.proprete" options="options" ></ui-knob>
									</div>
									<div class="form-group input-group-sm col-lg-3" style="float: left;">
										<span class="input-group-addon"><i class="fa fa-eye"></i> Rangement</span>
										<ui-knob value="nouvelEndroit.rangement" options="options" aria-describedby="sizing-addon3"></ui-knob>
									</div>
									<div class="form-group input-group-sm col-lg-3" style="float: left;">
										<span class="input-group-addon"><i class="fa fa-group"></i> Fréquentation</span>
										<ui-knob value="nouvelEndroit.frequentation" options="options" aria-describedby="sizing-addon3"></ui-knob>
									</div>

									<div class="form-group input-group-sm col-lg-3" style="float: left;">
										<span class="input-group-addon"><i class="fa fa-cube"></i> Général</span>
										<ui-knob value="nouvelEndroit.general" options="options" aria-describedby="sizing-addon3"></ui-knob>
									</div>
								</div>
						</div>
					</div>

				</div>

				<div class="panel-footer ">
					<div class="form-group text-right">
						 <form><input ng-model="nouvelEndroit.nom" placeholder="Nomme L'endroit" required></input><button ng-click="enregistrerEvaluation();showAlert()" class="btn-primary btn-block"><i class="fa fa-server"></i> Enregistrer</button></form>
						 <div  ng-show="alertMsg.show"  class="alert alert-success " role="alert" ><strong>Bien Joué</strong> Vous avez bien évalué le coin. </div>
                    </div>

                </div>
            </div>
        </div>
    </div>

	   <!-- TEMPLATE DE SELECTION DUN OBJET -->
	   <div class="col-lg-12"  id="liste">
		   <div class="panel panel panel-warning">
				<div class="panel-heading">
					<i class="fa fa-list"></i> Liste des évaluations

				</div>

				<div class="panel-body table-responsive" style="min-height:350px;">
					<table class="table table-bordered table-hover  " style="width:100%">

							<thead>
								<tr>
								   <th  >
										Nom
									</th>
								   <th>
										Adresse
									</th>
									<th  >
										<a href="#" ng-click="sortType = 'date'; sortReverse = !sortReverse">
										Date
										<span ng-show="sortType == 'date' && !sortReverse" class="fa fa-caret-down"></span>
										<span ng-show="sortType == 'date' && sortReverse" class="fa fa-caret-up"></span></a>
									</th>
									<th  >
										<a href="#" ng-click="sortType = 'proprete'; sortReverse = !sortReverse">
										Propreté
										<span ng-show="sortType == 'proprete' && !sortReverse" class="fa fa-caret-down"></span>
										<span ng-show="sortType == 'proprete' && sortReverse" class="fa fa-caret-up"></span></a>
									</th>
									<th  >
										<a href="#" ng-click="sortType = 'rangement'; sortReverse = !sortReverse">
										Rangement
										<span ng-show="sortType == 'rangement' && !sortReverse" class="fa fa-caret-down"></span>
										<span ng-show="sortType == 'rangement' && sortReverse" class="fa fa-caret-up"></span></a>
									</th>
									<th  >
										<a href="#" ng-click="sortType = 'frequentation'; sortReverse = !sortReverse">
										Fréquentation
										<span ng-show="sortType == 'frequentation' && !sortReverse" class="fa fa-caret-down"></span>
										<span ng-show="sortType == 'frequentation' && sortReverse" class="fa fa-caret-up"></span></a>
									</th>
									<th  >
										<a href="#" ng-click="sortType = 'general'; sortReverse = !sortReverse">
										Général
										<span ng-show="sortType == 'general' && !sortReverse" class="fa fa-caret-down"></span>
										<span ng-show="sortType == 'general' && sortReverse" class="fa fa-caret-up"></span></a>
									</th>

									 <th>
										Editer
									</th>
								</tr>
							</thead>
							<tbody>
								<tr ng-repeat="endroit in endroits | orderBy:sortType:sortReverse |filter:k">
									<td>{{endroit.nom}}</td>
									<td>{{endroit.adresse}}</td>
									<td>{{endroit.date| date:'MM/dd/yyyy @ h:mma'}}</td>
									<td>{{endroit.proprete}}</td>
									<td>{{endroit.rangement}}</td>
									<td>{{endroit.frequentation}}</td>
									<td>{{endroit.general}}</td>

									 <td><button ng-click="editerEndroit(endroit)" class= "fa fa-delete">supprimer</button></td>
								</tr>
							</tbody>
					</table>
				</div>

				<div class="panel-footer ">
					<div class="form-group">
						<!-- <input type="text" class="form-control " ng-model="k" placeholder="Chercher une adresse">  -->
					</div> <!-- <button class="btn">Rafraichir</button> -->
				</div>
			</div>	

		</div>	

<!-- FIN DE DIV GLOABLE FLUID -->
</div>

</body>

</html>

Le fichier PHP qui permet le stockage des photos est ici :

<?php

if ( !empty( $_FILES ) ) {

    $tempPath = $_FILES[ 'file' ][ 'tmp_name' ];
    $uploadPath = dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . $_FILES[ 'file' ][ 'name' ];

    move_uploaded_file( $tempPath, $uploadPath );

    $answer = array( 'answer' => 'File transfer completed' );
    $json = json_encode( $answer );

    echo $json;

} else {

    echo 'No files';

}

?>
Publicités