Tech Blog

A mobile app for librarians to get all the data about items by scanning their barcodes

The purpose of this mobile webapp  is to allow librarians to quickly and easely obtain informations about items by scanning their barcodes with their smartphones.

Technically, the app was made with the Apache Cordova/Phonegap environment, wich is a Javascript based Framework for creating cross-platforms applications. This means that the applications created in this way are Webviews coding in HTML/CSS/JS and that can then be build and deploy for Android or iOS environment. The Framework also provides a whole set of plugins (writed in JS) that allow to interact with the native components of the phone (camera, geolocation…).

The application was developed with the Ionic Framework  and also use Angular for routing and data binding. The first design was made with the online tool Ionic Creator wich is a very usefull drag-&-drop prototyping interface for creating the model of the app : css theming, routes parameters between pages, templating… . The free version of the tool is limited in features but sufficient to create the skeleton of the application

So what is the logic inside ?

The app embed a barcode reader (this cordova plugin) that allows to scan a barcode, and then get all the circulation information about the item (like number of loans) by requesting the read-item Aleph X-Service API with the barcode’s textual result in parameter. The requested url is

<your_server.name:port>/X?op=read-item&item_barcode=<barcode_parameter>

and the response of the X-Service is an XML result like this

<read-item>
 <z30>
  <z30-doc-number>104280</z30-doc-number>
  <z30-item-sequence>20</z30-item-sequence>
  <z30-barcode>0922107621</z30-barcode>
  <z30-sub-library>BU Lettres - Carlone</z30-sub-library>
  <z30-material>Livre</z30-material>
  <z30-item-status>Prêt 14/35 j.</z30-item-status>
  <z30-open-date>03/03/2009</z30-open-date>
  <z30-update-date>18/11/2011</z30-update-date>
  <z30-cataloger/>
  <z30-date-last-return>09/02/2018</z30-date-last-return>
  <z30-hour-last-return>08:51</z30-hour-last-return>
  <z30-ip-last-return>LASH</z30-ip-last-return>
  <z30-no-loans>008</z30-no-loans>
  <z30-alpha>L</z30-alpha>
  <z30-collection>RDC</z30-collection>
  <z30-call-no-type>1</z30-call-no-type>
  <z30-call-no>103 CAT</z30-call-no>
  <z30-call-no-key>103 cat</z30-call-no-key>
  <z30-call-no-2-type/>
  <z30-call-no-2/>
  <z30-call-no-2-key/>
  <z30-description/>
  <z30-note-opac/>
  ...
 </z30>
</read-item>

So in the app code we have :

Angular Services for the barcodescanner plugin and the treatment of the API CallIn the controllers file
angular.module('app.services', [])
.factory('barcodeScanner', function ($rootScope) {
    return {
      scan: function (onSuccess, onError, options) {
        cordova.plugins.barcodeScanner.scan(function () {
          var that = this,
            args = arguments;
          if (onSuccess) {
            $rootScope.$apply(function () {
              onSuccess.apply(that, args);
            });
          }
        }, function () {
          var that = this,
            args = arguments;
          if (onError) {
            $rootScope.$apply(function () {
              onError.apply(that, args);
            });
          }
        },
        {
          preferFrontCamera : false, // iOS and Android
            showFlipCameraButton : true, // iOS and Android
            showTorchButton : true, // iOS and Android
            torchOn: false, // Android, launch with the torch switched on (if available)
            saveHistory: true, // Android, save scan history (default false)
            prompt : "Placer un code-barre dans la zone de scan", // Android
            resultDisplayDuration: 500, // Android, display scanned text for X ms. 0 suppresses it entirely, default 1500
            orientation : "portrait", // Android only (portrait|landscape), default unset so it rotates with the device
            disableAnimations : true, // iOS
            disableSuccessBeep: false // iOS and Android
        });
      }
    };
  })

  .factory('serviceGetItem', ['$http', function ($http) {
    return{
      /*get the Json response*/
      cbApi: function(url){
        return $http.get(url);
      },
      docNumber: function(data){
        var docNumber = data.z30_doc_number;
            return docNumber;
        },
        bib: function(data){
          var bib = data.z30_sub_library;
              return bib;
          },
         loc: function(data){
            var loc = data.z30_collection;
                return loc;
            },
       cote: function(data){
          var cote = data.z30_call_no;
              return cote;
          },
       barcode: function(data){
            var barcode = data.z30_barcode;
                return barcode;
            },
       status: function(data){
            var status = data.z30_item_status;
                return status;
            },
      loans: function(data){
        var loans = data.z30_no_loans;
            return loans;
        },
      lastReturn: function(data){
          var lastReturn = data.z30_date_last_return;
              return lastReturn;
          },
       entryDate: function(data){
            var entryDate = data.z30_open_date;
                return entryDate;
            }
    }
  }])
angular.module('app.controllers', [])
  
.controller('cameraPageCtrl', ['$scope', '$http','$window', '$stateParams', '$rootScope','barcodeScanner',
function ($scope, $http, $window, $stateParams, $rootScope, barcodeScanner) {
//the scan function is activated on each link to the camera Page
        $scope.$on('$stateChangeSuccess', function (event, fromState) {
          if (fromState.name === "cameraPage") {
          barcodeScanner.scan(function (result) { 
            $scope.value = result.text;
            $window.location.href = '#/result/'+$scope.value;
                  });
          }
        });           
}])

.controller('resultPageCtrl', ['$scope', '$http', '$stateParams', 'serviceGetItem',
function ($scope, $http, $stateParams, serviceGetItem) {
$scope.cb = $stateParams.cb;
//this scanned barcode
serviceGetItem.cbApi(alephCbUrl+$scope.cb)
.then(function(response) {
  var pad = "000000000"
  var resultForCb = $.xml2json($.parseXML(response.data)).z30;
  console.log(resultForCb)
  $scope.docNumber = pad.substring(0, pad.length - (serviceGetItem.docNumber(resultForCb)).length) + serviceGetItem.docNumber(resultForCb)
  $scope.cote = serviceGetItem.cote(resultForCb)
  $scope.bib = serviceGetItem.bib(resultForCb)
  $scope.loc = serviceGetItem.loc(resultForCb)
  $scope.barcode = serviceGetItem.barcode(resultForCb)
  $scope.status = serviceGetItem.status(resultForCb)
  $scope.loans = serviceGetItem.loans(resultForCb)
  $scope.lastReturn = serviceGetItem.lastReturn(resultForCb)
  $scope.entryDate = serviceGetItem.entryDate(resultForCb)
}])
In the view templateAnd the displayed result
<div class="item item-divider">
 Ce CB : {{cb}}
</div>
<ion-item class="item-icon-left assertive">
  <i class="icon ion-ios-download"></i>Prets
     <span class="badge dark-bg">{{loans}}</span>
</ion-item>
<ion-item class="item-icon-left assertive">
  <i class="icon ion-android-locate"></i>Dernier retour
     <span class="badge dark-bg">{{lastReturn}}</span>
</ion-item>
<ion-item class="item-icon-left assertive">
   <i class="icon ion-android-calendar"></i>Date entrée
     <span class="badge dark-bg">{{entryDate}}</span>
</ion-item>

An interesting information is to know all the others items located for this record in the Aleph instance : we can retrieve them by passing the Aleph record doc number in another X-Service API called circ-status

<your_server.name:port>/X?op=circ-status&sys_no=<doc_number_value>&library=<library_code>

wich gives us all the others items’s barcodes as new parameters to be passed in input of the previous serviceGetItem Angular Service :

Services fileControllers file
angular.module('app.services', []) 
.factory('serviceGetItems', ['$http', function ($http) {
    return{
      itemsApi: function(url){
        return $http.get(url);
      }
    }
  }])
serviceGetItems.itemsApi(alephItemsUrl+$scope.docNumber)
.then(function(response) {
 $scope.exs = []
  var resultForAllCbs = $.xml2json($.parseXML(response.data)).item_data
  return othersExs = resultForAllCbs
  .filter(function (item) {
    return item.barcode != $scope.cb;
  })
  .map(function (item) {
    return {
      others: serviceGetItem.cbApi(alephCbUrl+item.barcode)
.then(function(response) {
var resultForOthersCb = $.xml2json($.parseXML(response.data)).z30;
$scope.exs.push({'bib':serviceGetItem.bib(resultForOthersCb),'loc':serviceGetItem.loc(resultForOthersCb),
'cote':serviceGetItem.cote(resultForOthersCb),'barcode':serviceGetItem.barcode(resultForOthersCb),
'prets':serviceGetItem.loans(resultForOthersCb),'retour':serviceGetItem.lastReturn(resultForOthersCb),
'statut':serviceGetItem.status(resultForOthersCb)})
$scope.nbExs = $scope.exs.length
})
    }
  })  
})
View templateResult
<div class="list card">
  <div ng-repeat="ex in exs">
    <div class="item item-divider">
        Autre exemplaire : CB {{ex.barcode}}
    </div>
    <ion-item class="item-icon-left item-text-wrap">
      <i class="icon"><img src="img/book_b.gif" /></i>
      <p><strong>Bibliothèque</strong> : {{ex.bib}}</p>
      <p><strong>Localisation</strong> : {{ex.loc}}</p>
      <p><strong>Cote</strong> : {{ex.cote}}</p>
      <p><strong>Prets</strong> : {{ex.prets}}</p>
      <p><strong>Dernier retour</strong> : {{ex.retour}}</p>
      <p><strong>Statut</strong> : {{ex.statut}}</p>
    </ion-item>
  </div>
</div>

Finally we need some bibliographic informations to complete the displayed view, and also to know, in the French academic context where the libraries are located in the national collective catalogue Sudoc, wich ones own the document too. The Aleph bibliographic record in Unimarc XML is obtained using the X-Service find-doc

<your_server.name:port>/X?op=find-doc&doc_num=<doc_number_value>&base=<library_code>

and the others locations provided by using a web service developed by the agency that manages the Sudoc catalogue.

Services fileControllers file
angular.module('app.services', [])
 .factory('serviceGetMarcoai', ['$http', function ($http) {
    return{
      docNumApi: function(url){
        return $http.get(url);
      },
      title: function(data){
           var title = data.filter(function (item) {
            return item.id == "200";
          })
          .map(function (item) {
            return item.subfield;
          })
               return title;
           },
       ppn: function(data){
                var ppn = data.filter(function (item) {
                 return item.id == "003";
               })
               .map(function (item) {
                 return item.subfield;
               })
                    return ppn;
                }
    }}])  
    .factory('serviceGetSudocLocs', ['$http', function ($http) {
      return{
        multiwhere: function(url){
          return $http.get(url);
        },
        libraries: function(data){
          var locs = [];
          $.each(data, function( i, item ) {
              locs.push({shortname:item.shortname,rcr:item.rcr,latitude:item.latitude,logitude:item.longitude}) ;
          });
          return locs;
          } 
      }}])
//bibliographic data
serviceGetMarcoai.docNumApi(alephMarcoaiUrl+$scope.docNumber)
.then(function(response) {
  var resultForMarcoai = $.xml2json($.parseXML(response.data)).record.metadata.oai_marc.varfield;
  $scope.title = serviceGetMarcoai.title(resultForMarcoai)[0].text
  $scope.ppn = serviceGetMarcoai.ppn(resultForMarcoai)[0].text.split("http://www.sudoc.fr/").pop()
  //Sudoc locations
  serviceGetSudocLocs.multiwhere(sudocMultiwhereUrl + $scope.ppn + "&format=text/json")
  .then(function(response) {
    var resultAllLocs = response.data.sudoc.query.result.library
    if(Array.isArray(resultAllLocs)) {
      $scope.libraries = serviceGetSudocLocs.libraries(resultAllLocs)
      $scope.countSudoc = $scope.libraries.length
    }
    else {
      $scope.countSudoc = "1";
        $scope.libraries = JSON.parse('[{"longitude":"'+resultAllLocs.longitude+'","shortname":"'+resultAllLocs.shortname+'","latitude":"'+resultAllLocs.latitude+'","rcr":"'+resultAllLocs.rcr+'"}]');
      }
  })
View templateResult
<div class="list card">
   <ion-item class="item-icon-left positive">
     <i class="icon"><img src="img/map_b.gif" /></i>Localisations Sudoc
        <span class="badge energized-bg">{{countSudoc}}</span>
    </ion-item>
   <ion-item class="item-icon-left dark" ng-repeat="librarie in libraries" style="font-size:80%;">
     <i class="icon ion-android-map"></i> {{librarie.shortname}}
  </ion-item>
</div>

Finally, adding some features like the possibility of archiving scanned references (in the Web local Storage) and sending by mail/sms, we’ve got a useful mobile app, for example to decide hand-delivered in shelving whether to keep a book in the library’s collection.

Leave a Reply