Laureline's Wiki

Laureline's Wiki

Rapport

Rapport

Virtual Comics Library (abrégé VCL) est une application qui permet de gérer et consulter une bibliothèque de bandes dessinées virtuelles.

Introduction

De nos jours, l’informatique est omniprésente et avec elle, le numérique. Alors que, il y a quelques années encore, pour regarder un film, écouter une chanson ou lire un livre, il était nécessaire d'acquérir le support physique correspondant, cette pratique s'avère aujourd'hui obsolète grâce à la numérisation des données. En effet, les données numériques semblent avoir peu à peu remplacé nos médias. Commençant DVDs et nos CDs, elles s’en prennent même aujourd’hui à nos livres grâce notamment aux smartphones et aux ebooks. Il semble dès lors presque naturel que les mangas et bandes dessinées n’y échappent pas.

Alors que de nombreuses applications sont disponibles pour la lecture et la gestion de bibliothèques pour la plupart des médias (Netflix pour les films, Spotify pour les musiques, Google play pour les livres), aucun logiciel ne se départage pour la gestion, le téléchargement et la lecture de mangas et bandes dessinées. L’objectif du projet VCL est de répondre à ce besoin dans le cadre du cours de projet.

:!: TODO: parler du cours de proj

Objectifs

Les objectifs de ce projet sont:

  • permettre de gérer plusieurs catalogues soit sur un système de fichiers local soit situé sur un serveur distant (en utilisant le protocole http)
  • permettre de classer les séries dans un arbre de catégories.
  • permettre d’ajouter des « tags » aux séries
  • permettre de lire un volume
    • Affichage en simple page ou double page
    • Sens de lecture GàD ou DàG
  • permettre de modifier les informations d’une série
  • permettre de créer une série vide
  • permettre d’adapter un fragment de site web en série ou volume
    • Effectue la création à l’aide de plugins
  • fournir une bibliothèque permettant d’abstraire les interactions avec les données.

Terminologie

  • Volume : Elément le plus petit dans la bibliothèque, Représente son équivalent physique (ensemble de pages reliées). Correspond à un fichier .cb* sur le système de fichiers.
  • Série : Ensemble de volumes et métadonnées associées.
  • Catégorie : Ensemble de catégories et séries.
  • Tag : Méta-information sur une série
  • Catalogue : Ensemble de Catégories et Séries situés dans un même emplacement physique sur un système de fichiers (distant ou local).
  • Bibliothèque : Ensembles de catalogues de l’application. Est spécifique à une installation.

Modules

  • libVCL : Fournit une bibliothèque d’abstraction des interactions avec les catalogues et leurs objets.
  • Bibliothèque : Permet la consultation et modification graphique du contenu des catalogues.
  • Liseuse : Permet la consultation d’un volume. Doit pouvoir être utilisé en standalone
  • Editeur : Permet la modification d’un volume. Doit pouvoir être utilisé en standalone.
  • Crawler : Permet le téléchargement d’un site vers un catalogue. Utilisation en ligne de commande possible.
 

Architecture

Modèle de Domaine

Le coeur de l'application se concentre sur la gestion d'une bibliothèque de livres virtuels.

<uml title=“Modèle de Domaine”> hide methods hide empty fields hide class circle hide interface circle

class Catalog class CatalogNode {

+name

} class Category class Series class VolumeMetadata {

+number
+title

} class SeriesMetadata {

+authors[*]
+artists[*]
+released
+summary

} class Tag {

+name

}

class Volume {

}

class Page {

}

CatalogNode <|– Category CatalogNode <|– Series

Catalog - “1” Category: root CatalogNode “*” – Category: children

CatalogNode - “*” VolumeMetadata: volumes VolumeMetadata *–> “0..1” SeriesMetadata: metadata Series *→ “1” SeriesMetadata: metadata

SeriesMetadata o–> “*” Tag: tags VolumeMetadata → Volume Volume –> Page </uml>

Une bibliothèque est divisée en plusieurs Catalogues qui représentent les différentes sources d'informations soit locales soit distantes. Chaque catalogue permet d'organiser ses enfants dans un arbre de Catégories et de Séries. Les catégories permettant de grouper les séries (et les catégories). La librairie n'impose pas de signification particulière quant aux critères de catégorisations. Un utilisateur pourrait par exemple choisir de catégoriser ses séries par genre alors qu'un autre par année de parution.

Les séries et les catégories peuvent toutes deux contenir des Volumes qui représentent un exemplaire physique. Etant donné que certaines maisons de publication éditent parfois des compilations de plusieurs chapitres (courant dans l'industrie du comics ou les “chapitres” individuels d'une douzaine sont publiés chaque semaine et sont ensuite republiés en un volume contenant tous les chapitres d'un arc) en un seul volume aucunne signification particulière n'est imposée à la notion de volume dans la librairie.

Chaque série, en plus de contenir ses volumes, possède aussi des informations concernant son ou ses auteurs et dessinateurs, son année de publication, un résumé de la série et les Tags qualifiant la série.

Un volume qui n'est pas situé dans une série mais une catégorie est considéré comme un One-Shot et possède alors les mêmes informations qu'une série.

Composants du Système

<uml title=“Composants du Système”>

package “ch.vcl.api” {

interface Catalog
component CatalogFactory
interface Volume
interface VolumeFactory
component VolumeLoader
component VolumeWriter
component VolumeStore
component ThumbnailStore

Catalog -- CatalogFactory

VolumeStore -- Catalog
ThumbnailStore -- Catalog
Volume "*" . Catalog

VolumeLoader -- VolumeFactory
VolumeWriter -- VolumeFactory

Volume <.. VolumeLoader: loads
Volume <.. VolumeWriter: writes

}

package “ch.vcl.crawler” {

interface CrawlerFactory
component Crawler
component Builder

CrawlerFactory --> Crawler: builds
Builder <. Crawler

}

package “ch.vcl.desktop” {

interface Config
component Library
component Reader
component Editor
component CrawlerUI

Library --> Reader
Library --> Editor
Library --> CrawlerUI

CrawlerUI - CrawlerFactory

Library -> Config
Config -> Catalog

}

</uml>

L'application est séparée en trois parties:

  • libvcl
    • api fournit un api permettant de manipuler les différents types de catalogues de manière transparente.
    • crawler fournit un api permettant de télécharger un série depuis un site de publication.
  • desktop est la partie graphique permettant d'afficher et manipuler les éléments de la librairie.

API libVCL

L'api permet de rendre transparents les différents modes de stockage des catalogues (XML, SQLite) ainsi que les différents emplacements (Local ou Distant HTTP).

  • CatalogFactory permet d'instancier les différents types de catalogues et leur objets dépendants de manière transparente pour les utilisateurs de l'API.
  • VolumeStore permet de charger un volume depuis le disque en fonction de ses meta-informations stockées dans le catalogue.
  • ThumbnailStore permet de récupérer les miniatures correspondant à un volume depuis ses meta-informations dans le catalogue.
  • VolumeLoader charge le contenu d'un catalogue en fonction de son moteur de stockage. Gère aussi une mise en cache en cas de données distantes pour permettre une consultation hors ligne.
  • VolumeWriter écrit le contenu d'un catalogue (local) en fonction de son moteur de stockage.

Crawler libVCL

Le crawler est un module permettant de composer une série à partir d'un site web (ex: mangafox.me).

  • CrawlerFactory fournit une instance d'un crawler approprié pour une url spécifiée.
  • Crawler parse la structure html du site web pour en extraire les informations sur la série concernée. Crée un descripteur contenant la liste des volumes et des images qui les composent.
  • Builder télécharge les images et compose les différents volumes de la série. Les volumes sont ensuite sauvegardés sur le disque et ajoutés au catalogue spécifié.

Desktop

L'application bureau affiche le contenu des différents catalogues des utilisateurs en utilisant les fonctionnalités fournies par l'API et le crawler.

  • Config stocke les préférences de l'applications tels que les emplacements des catalogues de l'utilisateur ainsi que ses préférences de lecture.
  • Library affiche le contenu des catalogues de l'utilisateur et permet de modifier les catalogues locaux.
  • Reader permet de lire un volume
  • Editor permet de créer/modifier un volume
  • CrawlerUI fournit une interface graphique pour l'utilisation du crawler.
 

Implémentation

Librarie libVCL

La libVCL à été concue pour fournir un maximum d'abstractions pour la gestion des catalogues pour permettre une forte séparation avec l'implémentation ce qui permettrai éventuellement à d'autre implémentations que celle de base de fournir les même fonctionnalités pour, par exemple, des plateformes mobiles qui n'ont pas été prises en compte lors de ce projet. Cette séparation permet aussi la gestion des différents moteurs de stockage et emplacements de manière complétement transparente pour l'utilisateur.

Design de l'API

L'API publique à été concue pour offrir un maximum de fonctionnalités aux utilisateurs sans compromettre la flexibilité d'implémentation. La seule classe offrant une implémentation concrète étant la CatalogFactory chargée d'instancier les bonnes sous-classes en fonction du type de catalogue.

Si plus d'options de stockage venaient à se présenter (ex. JSON), un refactor de la factory serait très probablement nécéssaire afin de mieux supporter des implémentations arbitraires.

La première partie de l'API est une copie conforme du modèle de domaine en termes d'implémentation. Elle fournit un moyen de parcourir et modifier les données du catalogue de manière simple.

<uml> hide empty methods hide empty fields

class CatalogFactory «Singleton» {

+ load(url, config?): Catalog
+ create(url, config?): Catalog

} CatalogFactory ..> Catalog : creates

abstract class VolumeStore {

+ getVolume(metadata, type)
+ addVolume(metadata, volume)
+ deleteVolume(metadata)
+ editVolume(metadata)

} VolumeStore ..> Volume : loads VolumeStore <.. VolumeMetadata

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

} Catalog –> Category : root

interface CatalogNode {

+ name

} Catalog ← CatalogNode : catalog CatalogNode - VolumeMetadata

CatalogNode <|– Category interface Category { } CatalogNode “*” – “1” Category

CatalogNode <|– Series interface Series { } Series → SeriesMetadata : metadata

interface VolumeMetadata {

+ number
+ title

} VolumeMetadata ← Volume VolumeMetadata –> “0..1” SeriesMetadata

interface SeriesMetadata {

+ authors[*]
+ artists[*]
+ released
+ summary

} SeriesMetadata –> Tag : tags

interface Tag {

+ name

}

interface Volume { } Volume –> “*” Page

interface Page {

+ contents

}

</uml>

La seconde partie de l'API permet d'interragir avec les différents éléments de stockage liés aux données d'un catalogue.

  • CatalogConfig configure les chemins du cache du catalogue. Les différents chemins fournis par cette classe sont passés aux autres composants.
  • INodeFactory permet de créer des classes correspondant au moteur de stockage du catalogue.
  • ICatalogWriter permet de sauvegarder le catalogue en utilisant son moteur de stockage.
  • VolumeStore gère le chargement, sauvegarde, ajout, import et supression de volumes dans le système de fichiers. Cetaines de ces opérations peuvent être indisponnibles selon l'emplacement du catalogue.
  • ThumbnailStore permet de récupérer la miniature associée à un volume. Selon l'implémentation elle sera soit extraite du fichier cbz ou téléchargée et mise en cache.

<uml> hide empty methods hide empty fields

class CatalogFactory «Singleton» {

+ load(url, config?): Catalog
+ create(url, config?): Catalog

} CatalogFactory ..> Catalog : creates CatalogFactory –> CatalogConfig : defaultConfig

class CatalogConfig {

+ cachePath

}

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

} Catalog → CatalogConfig : config Catalog –> INodeFactory Catalog –> ICatalogWriter Catalog –> VolumeStore Catalog –> ThumbnailStore

interface INodeFactory {

+ createCategory(name): Category
+ createSeries(name): Series
+ createVolume(number, name?): VolumeMetadata
+ createSeriesMetadata(): SeriesMetadata
+ createTag(): Tag

}

interface ICatalogWriter {

+ write()
+ writeTo()

}

interface Volume { }

class VolumeFactory «Singleton» {

+ getLoader(type): IVolumeLoader
+ getWriter(): IVolumeWriter

}

VolumeFactory ..> IVolumeLoader : creates VolumeFactory ..> IVolumeWriter : creates

interface IVolumeLoader {

+ load(path, metadata): Volume

} IVolumeLoader ..> Volume : loads

interface IVolumeWriter {

+ load(path): Volume
+ createEmpty(): Volume
+ write(path, volume)
+ createPage(volume): Page
+ setPageName(page)
+ setPageContents(page)

}

abstract class VolumeStore {

+ getVolume(metadata, type): Future<Volume>
+ addVolume(metadata, volume)
+ deleteVolume(metadata)
+ editVolume(metadata): Future<Volume>

} VolumeStore …> IVolumeLoader : uses

abstract class ThumbnailStore {

+ getThumbnail(metadata): Future<byte[]>

} </uml>

 

Moteurs de Stockage

La librarie implémente actuellement deux moteurs de stockage: XML et SQLite. Ces deux moteurs se conforment à l'API définie en implémentant les interfaces fournies.

Moteur XML

Le moteur de stockage XML permet de stocker les métadonnées sous la forme d'un ou plusieurs fichiers XML. Les fichiers de ce moteur peuvent être modifiés à la main par un utilisateur expérimenté. Le moteur de stockage XML se démarque par sa simplicité en terme d'implémentation.

L'avantage de ce moteur de stockage est qu'il peut être utilisé pour aussi bien des catalogues locaux que distants. Il est même possible de simplement transférer un catalogue XML d'un emplacement local sur un serveur HTTP et de le rendre disponnible comme catalogue distant.

Le catalogue est entièrement chargé en mémoire lors de sa lecture depuis le disque. Le parsing s'effectue à l'aide de l'API DOM fournie par java en utilisant principalement des requètes XPath pour facilement trier les défférent types de noeuds.

L'écriture s'effectue à l'aide de la librairie dom4j car l'API DOM java est difficile d'utilisation. Lors de son écriture, un catalogue est complétement ré-écrit sur le disque.

Un exemple de catalogue XML peut être trouvé sur le CD-ROM.

Moteur SQLite

Le moteur SQLite permet d'enregistrer des informations sur les catalogues sur une base de données SQLite.

Il se base sur le framework OrmLite. Voici son modèle UML:

<uml> hide empty fields hide empty methods

class category {

+ id: int {id}
+ parent_id: int {fk}
+ name: string

}

class series {

+ id: int {id}
+ parent_id: int {fk}
+ metadata_id: int {fk}
+ name: string

}

class series_metadata {

+ id: int {id}
+ summary: string
+ released: int
+ author: string
+ artist: string
+ tag: string

}

class volume_metadata {

+ id: int {id}
+ series_parent_id: int {fk}
+ category_parent_id: int {fk}
+ number: int
+ name: string

}

category – “*” category category – “*” series

series → series_metadata

category – “*” volume_metadata series – “*” volume_metadata </uml>

Malheureusement, l'utilisation d'OrmLite n'a pas apporté les résultats souhaités car les objets actifs liés à la base de données fonctionnent de manière particulière, ce qui est incompatible avec le fonctionnement de la libVCL.

Une solution possible est de créer, depuis nos objets actifs SQLite, des objets contenant les informations mais sans liaison à la base de données. Notre application pourrait alors travailler dessus en faisant abstraction de ces problèmes. Puis, lors de la sauvegarde, notre moteur n'aurait plus qu'à parcourir ces objets et les enregistrer.

Cette solution n'a pas été utilisée pour ce projet car elle était jugée plus compliquée que de travailler sur des objets actifs. Cependant, il s'est avéré, après avoir résolu les problèmes liés aux objets actifs, qu'elle était en réalité bien plus simple. Voici ci-dessous une description des problèmes rencontrés et la solution apportée.

Tout d'abord, il est impossible de créer des relations entre des objets qui ne sont pas enregistrés dans la base de données. Par exemple, si on crée un volume qui doit appartenir à une série et que celle-ci n'a pas encore été enregistrée dans la base de données, cela va lever une exception. La solution est qu'à chaque création d'objets, il faut immédiatement l'enregistrer dans la base de données. Ce qui a pour désavantage qu'il n'est pas possible de travailler sur des objets temporaires n'étant pas enregistrés.

Ensuite, une catégorie doit pouvoir contenir à la fois des séries et d'autres catégories. Cependant depuis SQLite, cela se traduit par deux collections distinctes, alors que notre API veut que nous puissions récupérer d'une fois la liste contenant les deux et de pouvoir ajouter un élément depuis celle-ci sans savoir lequel. Il a donc été nécessaire de créer un objet qui émule une liste contenant les deux listes et qui, à l'ajout d'un élément, récupère le type de celui-ci et l'ajoute à la bonne liste.

Enfin, lorsque l'on modifie des informations sur un enregistrement, il faut appeler une méthode qui met à jour cet élément sur la base de données et l'enregistrement lui-même. Le problème vient de cette dernière opération car quand OrmLite met à jour un élément il change les références vers les objets parents, ce qui a pour conséquence de corrompre la hiérarchie du catalogue.

Le dernier problème n'a pas été résolu car le seul moyen de le résoudre est d'implémenter une autre solution qui ne travaille pas avec des objets actifs. Ce qui reviendrait à recommencer le développement du début, alors que ce problème a été découvert à la fin du projet, après de longues heures de recherche.

Il en résulte que les catalogues SQLite ne sont accessibles qu'en lecture et qu'il faut redémarrer l'application à chaque modification du catalogue.

 

Gestion des Catalogues

Il existe deux types de catalogues: Locaux et Distants. Un catalogue local est disponible en écriture ET en lecture tandis qu'un catalogue distant n'est disponible qu'en lecture. Un catalogue est définit par son URL de base. Deux catalogues différents ne devraient pas tenter de partager une même racine en écriture.

<uml> hide empty members

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

}

Catalog <|– LocalCatalog Catalog <|– RemoteCatalog

LocalCatalog –> LocalVolumeStore RemoteCatalog –> RemoteVolumeStore

LocalCatalog –> LocalThumbnailStore RemoteCatalog –> RemoteThumbnailStore </uml>

Un catalogue local stocke toute ses informations dans sa racine. Le dossier dans lequel se trouve le fichier racine du catalogue détermine l'emplacement de stockage des volumes de ce catalogue. Etant donné que tous les chemins référencés dans un catalogue sont relatifs à son dossier de base, il est possible de déplacer un catalogue sur le système de fichiers sans que les métadonnées ne soient corrompues.

Un catalogue distant est très semblable à un catalogue local, excepté qu'il n'est disponible qu'en lecture seule pour ses utilisateurs. Il est d'ailleurs possible de copier les fichiers d'un catalogue local au format XML sur un point d'accès HTTP et il sera alors disponible en tant que catalogue distant, il sera néanmoins nécessaire de générer les miniatures pour qu'elles soient disponibles.

 

Gestion des Volumes

La gestion des volumes utilise l'api 7zip-JavaBindings. Cette api permet d'interragir avec 7zip, logiciel gratuit, permettant de décompresser et compresser beaucoup de format d'archives actuels, tel que rar, 7zip, zip, bzip2, etc. 7zip est par ailleurs disponible sur tous les Systèmes d'exploitations courants. 7zip-JavaBindings est aussi disponible sur les systèmes d'exploitation courants (windows, linux, mac osx et arm).

Ce choix a été motivé par l'existance même de cette api. En effet, le fait de devoir potentiellement traiter plusieurs types d'archives aurait été compliqué à implémenter en java et pas nécessairement supporté par java de base.

C'était un choix risqué à prendre, car personne dans le groupe n'avait utilisé 7zip-JavaBindings auparavant. Nous ne savions donc pas comment l'api allait réagir, si elle avait des bugs ou pas, etc.

7zip-JavaBindings fonctionne en mode 1-1, c'est à dire que lors de l'extraction d'une archive, l'on doit appeler systématiquement la fonction extractSlow() qui permet d'extraire un seul fichier de l'archive à la fois. Cette fonction est lente, d'où son nom, mais comparé à l'autre solution, qui se comporte comme une approche c++, cette solution parraissait la plus simple. Après quelques tests il s'est avéré que cette fonction n'est pas si lente que cela. A cause de ce comportement de 7zip-Javabindings, chaque archive doit être extraite via une boucle, qui permet de traverser l'entier des fichiers présents dans l'archive.

Afin d'éviter de trop surcharger l'application, qui peut tourner sur un pc plus lent que les nôtres, un système de Lazy loading a été mis en place afin de ne pouvoir charger qu'une seule partie d'un volume et non l'entier. Ce système prend en paramètre une certaine plage de page à charger et permet de charger ces dernières uniquement. Au fur et à mesure de l'avancement dans les pages, les suivantes peuvent être chargées dynamiquement.

L'architecture des volumes se présente comme suit:

<uml> hide empty fields hide empty methods

Interface Volume Volume : getPages(): List<Page> Volume : getMetadata(): VolumeMetadata

Volume <|– LazyVolume LazyVolume : loadPage(int index) LazyVolume : loadPages(int start, int end)

Volume <|– EagerVolume

Volume <|– MutableVolume MutableVolume : createPage(): Page

Volume *- Page :contains

Interface Page Page : getImage(): byte[] Page : setPath() Page : compareTo(Page other): int

Page <|– LazyPage Page <|– EagerPage Page <|– MutablePage MutablePage : getFilename(): String MutablePage : setImage(byte[] i) MutablePage : setFilename(String name)

MutableVolume <.. VolumeWriter :creates

EagerVolume <.. EagerVolumeLoader :creates LazyVolume <.. LazyVolumeLoader :creates

Interface IVolumeLoader IVolumeLoader : load(String path, VolumeMetadata metadata): Volume EagerVolumeLoader –|> IVolumeLoader LazyVolumeLoader –|> IVolumeLoader

Class ImageReader ImageReader : write(byte[] data): int ImageReader : makePage(): Page LazyVolumeLoader …> ImageReader :uses EagerVolumeLoader …> ImageReader :uses

Class “7zip-JavaBindings” as 7jb ImageReader ..> 7jb :uses VolumeWriter …> 7jb :uses LazyVolumeLoader …> 7jb :uses EagerVolumeLoader …> 7jb :uses

Interface IVolumeWriter IVolumeWriter : load(String path): Volume IVolumeWriter : write(String path, Volume volume) IVolumeWriter : createEmpty(): Volume IVolumeWriter : createPage(Volume volume): Page IVolumeWriter : setPageName(Page page, String name) IVolumeWriter : setPageContents(Page page, byte[] content) VolumeWriter –|> IVolumeWriter

</uml>

Comme on peut le voir sur le schéma ci-dessus, les volumes sont composés de pages. Ces dernières sont de type différents selon les différents types de volumes. Un EagerVolume contiendra des EagerPage, un LazyVolume des LazyPage et ainsi de suite. Chaque type de page est légèrement différent d'un autre, car ils ne servent pas les mêmes usages.

Nous pouvons aussi observer que les deux VolumeLoader (Eager et Lazy) utilisent tous-deux la classe ImageReader. Cette dernière fait le lien entre les 7zip-JavaBindings et la librairie. Cette classe est particulièrement utilisée pour récupérer les données de l'archive ouverte. Elle contient notamment la fonction write(byte[] data) qui permet à 7zip d'aller écrire dans ce byte array ce qu'il a lu dans le fichier en extraction (une archive peut évidement contenir plusieurs fichiers). Lors de l'écriture c'est un peu plus compliqué. Le VolumeWriter appèle une fonction 7zip-JavaBindings, qui elle même va appeler une fonction de l'api 7zip. Cette fonction 7zip va ensuite rappeler via un callback la classe StatusCallback (classe privée de VolumeWriter), qui contient les fonctions getStream(int index) et getItemInformation(int index, OutItemFactory<IOutItemZip> outItemFactory). Ces dernières permettent à 7zip de créer le fichier dans l'archive, avec les bonnes permissions, bon chemin, etc.

Petit bémol, en travaillant directement avec l'archive, les ficheirs sont lus dans leur ordre au sein de l'archive. Ceci ne garantit pas du tout le bon ordre des pages d'un volume. En effet si la page 2 est enregistrée avant la 0 et la 1, alors le premier fichier lu sera la page 2 et non la page 0 comme on pourrait s'y attendre. Ceci a dû être traité lors du chargement du volume. Le EagerVolumeLoader chargeant toutes les pages d'un coup, il n'a pas été difficile de trier ses pages une fois chargées en mémoire. Le LazyVolumeLoader permettant de ne charger qu'un certain nombre de pages, ne permet pas de trier aussi facilement les pages. En effet, il suffit que la page 0 soit la dernière, alors les 10 premières pages peuvent être les pages 1 à 10, mais devraient être les pages 0 à 9. Une solution simple serait de charger, trier et réécrire le volume, mais c'est très peu propre et non-performant. Ceci posant un sérieux problème lors du chargement du volume, le chargement en mode Lazy n'a pas été implémenté dans la liseuse (qui devrait dans ce mode demander les pages suivantes à chaque changement de page affichée).

 

Crawler

Le crawler est un module supplémentaire fournit par la librairie qui permet de télécharger une série à partir d'un site web. Ceci permet la lecture “on the go” de séries sans la nécessité de posséder une connexion internet.

Le fonctionnement et la structure du crawler est une ré-implémentation du projet MangaArchive qui a inspiré la création de cette application.

<uml> hide empty fields hide empty methods

class CrawlerFactory {

+ register(crawler)
+ registerAll()
+ forUrl(url): Optional<ICrawler>
+ getExecutor(): Executor
+ setExecutor(executor)

} CrawlerFactory ..> ICrawler : provides

interface ICrawler {

+ canCrawl(url): boolean
+ crawl(url): WebSeries
+ crawlAsync(url): ProgressFuture<WebSeries>

} ICrawler .> WebSeries : creates

class Builder {

+ build(webSeries): boolean
+ buildAsync(webSries): ProgressFuture<boolean>

} WebSeries .> Builder

WebSeries –> WebVolume WebVolume –> WebChapter WebChapter –> WebPage

</uml>

Le module fonctionne en deux temps: Crawling et Building. La phase de crawling construit un descripteur à partir des informations disponibles sur la page de la série. Un descripteur contient les métadonnées de la série construite en “best effort” et la référence vers toutes les images (pages) nécessaires à la construction de la série. Les images sont organisées en chapitres eux mêmes organisés en volumes. L'inclusion des chapitres dans la structure permet à certaines implémentations de mieux gérer la récupération des URLs des pages. Une fois la création du descripteur complet il est possible de le sauvegarder au format JSON pour une utilisation future, par exemple si le processus de construiction est interrompu. La phase de building utilise le descripteur créé lors de la phase précédente, effectue le téléchargement de toutes les images et les ajoute à un nouveau volume qui est alors ajouté à la série. Une fois le processus terminé, le série est ajoutée à la racine du catalogue et peut être modifiée par l'utilisateur.

Chacun des deux modules fournit deux modes de fonctionnement: Synchrone et Asynchrone. Le mode synchrone fontionne en appelant le mode Asynchrone et en bloquant sur le résultat. Le mode asynchrone détache son fonctionnement en arrière-plan dans l'executor fournit par la CrawlerFactory et retourne un ProgressFuture. L'objet ProgressFuture hérite de CompletableFuture et permet en plus de reporter l'avancement de la tâche aux objets s'étant enregistrés pour recevoir les notifications de progrès.

Gestion de Progression

La gestion de la progression se fait au travers d'une classe héritant de CompletableFuture. Les Promesses et Futures sont déjà fortement utilisées dans d'autre parties de l'application ou les opérations asynchrones sont nécessaires, par exemple, lors du chargement d'un volume. Dans le cas courant, en plus de fournir la promesse d'une réponse, étant donné que l'opération peut durer plusieurs minutes, elle doit aussi fournir à son appelant un moyen de surveiller l'avancement de l'opération. Une option aurait été de créer une classe spécialisée pour cette tâche, mais cela aurait nécessité à l'appelant de devoir gérer un objet supplémentaire. L'option choisie à été de créer une classe ProgressFuture qui dérive de CompletableFuture et qui fournit la possibilité à la tâche de communiquer son avancement à travers un canal asynchrone à son appelant.

<uml> hide empty fields hide empty methods

class CompletableFuture<TValue> {

+ whenComplete(consumer)
+ whenCompleteAsync(consumer, executor)

}

CompletableFuture <|– ProgressFuture class ProgressFuture<TValue> {

+ whenProgress(consumer)
+ whenProgressAsync(consumer, executor)
+ whenMessage(consumer)
+ whenMessageAsync(consumer, executor)

}

</uml>

 
 

Application Desktop

La partie graphique de l'application à été réalisée sous la forme d'une application JavaFX. Nous avons choisi le design “single-window” afin de fournir une application avec une interface unie. Les composant communiquent entre eux à l'aide d'un Message Bus pour permettre un certain découplage des responsabilités entre, par exemple, le composant demandant l'ouverture d'un onglet pour un volume et le composant qui crée les onglets dans sa vue.

Annecdote de développement Durant les tests d'utilisation, nous avons remarqué que l'application accumulait beaucoup de mémoire qu'elle ne libérait jamais. Après une analyse avec un profiler Laureline à découvert que le message bus Google Guava gardait des références fortes sur tous les objets enregistrés (implementation) et ne les relachais jamais. Nous avons donc changé de librarie pour MBassador ce qui à résolu les problèmes de fuite mémoire.

Bibliothèque

Bibliothèque

La bibliothèque est implémentée à l'aide de plusieurs contrôleurs. Un contrôleur principal qui gère la TreeView qui affiche les catalogues et un contrôleur secondaire qui se charge de l'affichage des volumes. Chaque volume possède aussi son propre contrôleur afin de pouvoir gérer l'affichage et les actions d'un volume de manière isolée.

Affichage des Catalogues

L'affichage des catalogues utilise une TreeView pour afficher les catalogues. Pour pouvoir appliquer des filtres au contenu de la TreeView, les classes RootCatalogItem et CatalogItem on été créées. La classe RootCatalogItem se place comme élément racine (La TreeView est configurée pour ne pas afficher son élément racine mais uniquement ses enfants) et expose comme enfants les catégories racines des catalogues enregistrés dans le système sous la forme de CatalogItems qui eux mêmes possèdent plusieurs CatalogItems enfants. Ceci forme un arbe parallèle à la structure des catalogues sous-jacents. Ces objets peuvent être considérés comme des “proxy” vers la vértiable structure.

<uml> hide empty fields hide empty methods

TreeView → “1” RootCatalogItem RootCatalogItem → “*” CatalogItem CatalogItem –> “*” CatalogItem CatalogItem –> CatalogNode RootCatalogItem –> “1” Catalog Catalog → “1” Category CatalogNode <|– Category CatalogNode <|– Series </uml>

Ces objets proxy permettent d'implémenter un système de filtre sur l'arborecence sans qu'il soit nécessaire de recréer toute la structure. La classe CatalogItem expose une méthode refresh() qui synchronise ses enfants avec l'objet sous-jacent et conserve l'ordre des enfants.

Filtrer un Catalogue

Une méthode applyFilter(filter) est aussi disponible sur les CatalogItems. Cette méthode permet de restreindre quels noeuds sont affichés. Le filtre est appliqué de manière récursive aux enfants. Le noeud est considéré comme ayant une correspondance si il remplit les critères du filtre ou si un de ses enfants remplit les critères du filtre. Si un enfant ne correspond pas au filtre il est retiré de la liste des enfants du noeud (si il y figure), de même si un enfant correspond aux critères il sera ajouté à la liste des enfants (si il n'y figure pas). La liste des enfants est ensuite retriée en fonction de l'ordre d'origine.

Ce fonctionnement permet d'afficher des noeuds qui correspondent au filtre même si leur parents ne le sont pas.

Modification du Catalogue

Modification d'une série En plus d'afficher la structure d'un catalogue, la bibliothèque permet aussi d'en modifier le contenu si il est local.

La bibliothèque présente des dialogues lors de la modification ou la création d'éléments. Un système pour gérer les dialogues “complexes” a été mis en place.

<uml> hide empty fields hide empty methods

abstract class Controller

Controller <|– DialogController abstract class DialogController<TResult> {

+ getTitle()
+ getButtons()
+ getResult(button)

}

DialogController <|– ModalController abstract class ModalController<TValue> {

+ setValue(value)
+ getValue()
+ commit()

}

</uml>

Le DialogController permet de présenter un dialogue en mode modal et renvoie un résultat. Ce type de dialogue permet de renvoyer un resultat en fonction du bouton qui a été appuyé. Le résultat est transformé en optional en fonction de sa valeur (en utilisant Optional.ofNullable()).

Le ModalController présente un dialogue “Ok, Cancel” en mode modal avec une valeur existante et permet au contrôleur de la modifier lorsque l'utilisateur sélectionne “Ok” via la méthode commit(). Si l'utilisateur annule l'action, la méthode n'est pas appelée et l'objet ne sera donc pas modifié.

Les différents contrôleurs d'ajout et de modification des objets du catalogues héritent de ces deux classes afin de faciliter leur implémentation.

Affichage des Volumes

Les volumes sont affichés dans une TilePane. Chaque volume est un tile qui affiche sa couverture (obtenue à l'aide du ThumbnailStore de son catalogue) ainsi que son titre.

 

Liseuse

Liseuse

La liseuse permet de lire les différents volumes de la bibliothèque. Elle est à part de le bibliothèque et ne fait que prendre un volume en entrée. Elle offre à l'utilisateur plusieurs option de navigation telle que:

  • Changer le sens de lecture: Cela permet, pour certains mangas, de lire de droite à gauche plutôt que de gauche à droite. Cela est surtout pratique quand on utilise les doubles pages.
  • Simple ou double pages: Cela gère l'affichage et définit si une seule page à la fois ou deux pages en même temps doivent être affichées. L'affichage de deux pages simultanées simule l'expérience qu'un utilisateur pourrait avoir si il lisait un livre physique.
  • Redimmensionnement de la page: Il est possible de définir le redimmensionnement automatique de la page sois sur la hauteur, sois sur la largeur de la fenêtre.

L'implémentation de la logique de la liseuse est faites grâce à un Navigator implémenté dans le package navigation. La classe Navigator étant abstraite, elle donne la possibilité de créer des Navigator pour un nombre de pages voulu. Nous avons décidé de ne l'implémenter que pour les pages simple (SimpleNavigator) et pour les pages doubles, c'est à dire 2 pages simples (DoubleNavigator) car cela n'a pas de sens dans notre contexte de l'utiliser pour plus de pages.

Chaque Navigator contient un volume et une page courante qui est affichée. Ces pages sont définie au tant que DisplayPage, qui est aussi une classe abstraite permettant d'implémenter des classes définies sur un nombre de pages voulu. Nous avons implémenté les classes pour des pages simples (SimplePage) et des pages double, c'est à dire 2 pages simples (DoublePage). Une fois encore, nous n'avons pas implémenté d'autre classe, cela n'ayant pas de sens pour nous mais cela serait tout à fait possible.

En résumé, le Navigator permet d'implémenter la logique par laquelle les pages doivent être parcourues alors que les DisplayPage permettent simplement de contenir une page de manière contrôlée et définie dans le programme.

Voici un diagramme de classe pour le Navigator:

<uml> hide empty fields hide empty methods

abstract class Navigator{

  1. vol: Volume
  2. direction: Direction
  3. currentPage: DisplayPage

+setDirection(sens: Direction) : void

+setCurrentPage(page: DisplayPage) : void
+getVolume() : Volume
+getDirection(): Direction
+getCurrentPage(): DisplayPage

} class SimpleNavigator {

+SimpleNavigator(volume: Volume, direction: Direction, beginIndex: Int)
+SimpleNavigator(nav: DoubleNavigator)
+nextPages(): DisplayPage
+previousPages(): DisplayPage
+goTo(index: int): DisplayPage

} class DoubleNavigator {

+DoubleNavigator(volume: Volume, direction: Direction, beginIndex: Int)
+DoubleNavigator(nav: SimpleNavigator)
+update(): void
+nextPages(): DisplayPage
+previousPages(): DisplayPage
+goTo(index: int): DisplayPage

} interface INavigator {

+nextPages() : DisplayPage
+previousPages() : DisplayPage
+goTo(index: int): DisplayPage
+setDirection(sens: Direction) : void
+setCurrentPage(page: DisplayPage) : void
+getVolume() : Volume
+getDirection(): Direction
+getCurrentPage(): DisplayPage

}

enum Direction{

LTR,RTL

}

Navigator –> SimpleNavigator Navigator –> DoubleNavigator INavigator –> Navigator </uml>

On peut voir que l'on utilise une interface INavigator qui oblige les implémentations de définir les méthodes essentielles à la logique d'un navigateur. Le Navigator implémente partiellement certaines méthodes qui sont communes à n'importe quel navigateur.

Voici un diagramme de classe pour le DisplayPage: <uml> hide empty fields hide empty methods

abstract class DisplayPage{

  1. imageIndexes: ArrayList<Integer>
  2. imageReferences: ArrayList<Image>
  3. direction: Direction

+DisplayPage(indexes: ArrayList<Integer>, images: ArrayList<Image>, dir: Direction)

+getImageIndexes(): ArrayList<Integer>
+getImageReferences(): ArrayList<Image>
+getDirection(): Direction

} class SinglePage {

+SinglePage(index: int, image: Image, dir: Direction)
+getImage(): Image
+getIndex(): int

} class DoublePage {

+DoublePage(index1: int, index2: int, image1: Image, image2: Image, dir: Direction)
+getFirstImage(): Image
+getSecondeImage(): Image
+getFirstIndex(): int
+getSecondIndex(): int

} interface IDisplayPage {

+getImageIndexes(): ArrayList<Integer>
+getImageReferences(): ArrayList<Image>
+getDirection(): Direction

}

DisplayPage –> SinglePage DisplayPage –> DoublePage IDisplayPage –> DisplayPage </uml>

Ici, la logique est la même que pour le navigateur. Une interface définissant les méthodes obligatoires et une classe DisplayPage qui permet de faire une implémentation partielle des pages.

Une subtilité réside dans le DoubleNavigator. En effet, il est possible que des page doubles, c'est à dire des pages qui, dans un livre physique sont en paysage, se trouvent dans le volume. Ces pages ont été mises par les gens qui ont numérisé le livre comme une grande page qui est plus large que haute. Cela a été fait pour conserver le travail de l'auteur et ne pas dénaturer ces images en les séparant sur deux pages différentes. Dans ce cas, si jamais le DoubleNavigator voit qu'il y'a une double page, il va uniquement afficher la prochaine page et non les 2 prochaines pages. Cela à pour effet que la page courante dans le navigateur sera une SimplePage et non une DoublePage comme on pourrait s'y attendre. Avec ce système, la vue n'a pas à ce soucier du type de navigateur qu'elle utilise, étant donné qu'elle reçoit une DisplayPage et de ce fait, il lui suffit de faire un contrôle du type de page qu'elle a pour donner le bon affichage.

Exemple d'image simple

Exemple d'image double

Redimensionnement

La liseuse offre trois options de redimensionnement: Complet, Horizontal, Vertical. Le mode complet affiche la/les page(s) courante(s) au complet sur la fenêtre. Le redimensionnement Horizontal agrandi l'image jusqu'à ce que sa largeur soit celle de la fenêtre, l'utilisateur peut alors faire défiler l'image verticalement. De même, le mode vertical effectue un redimensionnement par rapport à la hauteur de l'image.

La logique est implémentée dans la fonction ReaderController#refreshScaling() qui prend en charge tout les modes ainsi que le mode simple et double page. Cette fonction est appellée quand l'utilisateur change les dimensions de la fenêtre et lors d'un changement de page.

 

Editeur

Editeur

L'editeur à été implémenté de manière très simple. Il présente la liste des images présentes dans l'archive avec une aperçu lorsqu'elle est sélectionnée.

Il est possible d'ajouter une ou plusieurs images à l'aide du bouton “Add” et supprimmer une page à l'aide du bouton “Delete” ou de la touche backspace. Le bouton “Save” écrit le volume sur le disque à l'aide du IVolumeWriter.

Cette présentation à été préférée à celle présentée dans le cahier de charges car elle est beaucoup plus simple à gérer au niveau backend sans sacrifier la facilité d'utilisation. Il avait été prévu d'implémenter un Drag & Drop pour permettre le réagencement des pages, mais étant donné que les fichiers sont triés par nom avant la lecture, il est facile de réordonner les fichier en changeant leur noms.

 

Crawler

Crawler

Le crawler est implémenté comme un module “standalone” qui peut être lancé depuis le menu principal de l'application. Il implémente une interface pour intéragir avec le module de l'API.

Les boutons sont mis à jour en fonction de l'état du système en utilisant les bindings javafx afin de gérer facilement les mises à jour.

Cette interface utilise les procédures asynchrones fournies par l'API. Ceci permet à l'interface de reporter à l'utilisateur la progression des différentes étapes de manière fluide.

ProgressFuture<WebSeries> future = crawler
  .crawlAsync(url)
  .whenProgressAsync(this::onCrawlProgress, PlatformExecutor.instance)
  .whenMessageAsync(this::onCrawlMessage, PlatformExecutor.instance)
  .whenCompleteAsync(this::onCrawlComplete, PlatformExecutor.instance);
  
private void onCrawlProgress(ProgressFuture<WebSeries> future) {
  // Met à jour la barre de progression
  crawlProgress.setProgress(future.getProgress());
  crawlCount.setText(String.format("%d/%d", future.getCurrent(), future.getMaximum()));
}
    
private void onCrawlMessage(ProgressFuture<WebSeries> future) {
  // Affiche le message dans le log
  log.appendText(future.getMessage() + "\n");
}

L'utilisation des méthodes de rapport asynchrones (whenCompleteAsync, whenProgressAsync, …) en conjonction avec un Executor spécial permettant aux méthodes d'être executées dans le contexte de l'UI permet une gestion très simple du rapport de progression.

L'interface permet aussi de charger et sauvegarder des descripteurs dans le pipeline afin de permettre aux utilisateurs de conserver les métadonnées pour un téléchargement ultérieur.

 
 
 

Améliorations & Modifications

Bien que l'application soit parfaitement utilisable en l'état, de nombreuses améliorations et modifications ont été discutées mais elles n'ont malheureusement pas pu être implémentées, soit car elles sortaient du cadre du projet soit par manque de temps.

Architecture de Plugins

Temps de développement estimé: 2 semaines à plein temps pour la conception/prototypage et 3 semaines pour l'implémentation.

Actuellement, la logique de l'application est entièrement contenue. Fournir la possibilité de charger des plugins externes à travers un mécanisme de chargement post-lancement pourrait permettre aux utilisateurs de charger des fonctionnalités supplémentaires sans la nécessité de distribuer une nouvelle version de l'application.

Malheureusement, la conception et la réalisation d'une architecture de plugins complète, à la fois pour l'API et pour l'application desktop, à été considérée comme sortant du cadre du projet.

Catalogues

Des types de catalogues supplémentaires qui pourraient prendre en charge, soit des options de stockage de metadonnées différents ou des systèmes de distribution du contenu alternatifs.

Metadonnées

Le format JSON est extrèment populaire et serait un candidat idéal pour le stockage d'un catalogue. La possibilité d'effectuer une mise à jour incrémentale des métadonnées ainsi que du contenu des catalogues à aussi été envisagée mais aurait nécessité une infrastructure côté serveur plus importante que ce que le temps nous permettait et à donc été écartée.

Stockage

Un catalogue supportant une authentificaion permettrait à certains utilisateurs de partager leurs catalogues mais sans que celui-ci ne soit disponible à tout le monde. Ceci nécessite malheureusement la possibilité pour un catalogue d'effectuer des requêtes auprès de l'utilisateur (demander un mot de passe). La mise en place d'un tel mécanisme aurait été couteux en temp de développement, l'idée a donc été abandonnée.

Supporter un mécanisme de DRM pourrait permettre à la plate-forme d'être utilisée par des maisons de publications soucieuses du piratage de leur contenu. L'implémentation d'un tel système pourrait être un projet en lui-même étant donné sa complexité et a donc aussi été abandonné.

Types de contenu

Supporter plus de types de contenu susceptibles de se trouver dans une bibliothèque, tels que le format epub, a aussi été soulevé. En soit, la gestion de plusieurs types de contenu serait principalement un problème au niveau de la liseuse qui nécessiterait le support pour plusieurs moteurs de rendu différents ce qui augmenterait drastiquement la complexité de l'application, surtout dans le cas d'une liseuse epub.

 

Conclusion

Au terme de ce projet, nos sentiments sont positifs. Nous avons réussi à réaliser un projet d'envergure en groupe. Nous avons tous acquis de nombreuses connaissances ou compétences nouvelles.

Pour la plupart d'entre nous, nous n'avions jamais travaillé à plus que deux ou trois personnes sur un projet. Travailler en groupe, comme nous l'avons expérimenté, nécessite bien plus de compétence que ce que nous pensions. En effet, cela demande une organisation et une communication bien plus importante. Bien que nous ayons commis des erreurs, cette expérience nous a permis de les cibler et de savoir nous améliorer pour les projets à venir.

 

Gestion du Groupe

Etant donné la grande diversité de personnalités et de niveaux présentes dans le groupe, sa gestion n'a pas toujours été facile. De manière générale les membres du groupe avaient quelques lacunes au niveau de la conception de systèmes orientés objets en utilisant des principes tels que la séparation des reponsabilités et les patrons de conception du Gang of Four (GoF). Le problème vient d'un manque de pratique dans les concepts mentionnés précédemment. Les membres du groupe ont pu profiter de cette expérience pour élargir leurs connaissances.

Les compétences techniques n'étaient pas le seul problème du groupe. Les aspects sociaux ont aussi posé quelques problèmes. Etant donné la taille du groupe, il était inévitable que certaines personnalités entrent en conflit. Certains membres du groupe étaient particulièrement “possessifs” des parties d'application qu'ils avaient réalisés et se trouvaient offusqués lorsque d'autres membres du groupe y apportaient des modifications, même si la raison était justifiée. Un autre problème est né des personnalités “procrastinatrices” car certains membres du groupe attendaient le dernier jour avant la réunion de projet pour avancer leurs travaux et se heurtaient innévitablement à des problèmes qu'ils n'avaient pas pu anticiper. Ceci retardait évidemment l'avancement de la tâche. Heureusement pour le projet cette attitude n'a pas persisté au delà des permières semaines de travail.

La gestion de la dynamique de groupe par la cheffe de projet a certainement aussi contribué à certains des problèmes mentionnés précédemment. Premièrement, l'attitude “Hands-off” adoptée n'a pas été avantageuse, car les membres du groupe étaient principalement inexpérimentés. Deuxièmenent, les attentes vis à vis des personnes n'ont pas tenu compte de leur expérience et leurs connaissances, laissant ainsi le partage des tâches inégal. Ces erreurs de jugement ont conduit à un déséquilibre de la charge de travail.

Le manque de communication entre les membres du groupe a aussi généré un certain nombre de problèmes. Heureusement, les réunions hebdomadaires ont empêché le problème de prendre une ampleur trop grande.

En conclusion, malgré les problèmes rencontrés par l'équipe, le projet à pu être mené à bien dans le temps imparti.

Laureline David

Ce projet est à la fois un échec et une réussite!

De son inception à sa réalisation ce projet à été pour moi à la fois très intéressant mais aussi une grande source de stress. Prendre la tête d'une équipe nécessite un ensemble de compétences avec lequel je ne suis pas entièrement familière et cela à parfois généré des tensions au sein de l'équipe. Ceci est, je pense, principalement dû aux attentes que j'avais vis-à-vis des autres membres de l'équipe, autant en terme de compétences techniques qu'en terme d'investissement personnel dans le projet. Mon obsession fort intérêt dans la réussite du projet à très certainement obscurci mon jugement à certains moments.

Cette conclusion est beaucoup plus déprimante que je ne l'avais immaginé.

 

Yves Athanasiadès

Ce projet m'a permis dans un premier temps d'expérimenter le travail en équipe lors de la création d'un programme. Ayant déjà travaillé 4 ans avant l'école d'ingénieur, j'avais déjà par le passé participé à des projets en entreprise. Ces derniers étant complètement différent au niveau de leur grandeur, je n'ai pas pu participer comme dans ce projet-ci. Je dois dire que participer un petit peu à un grand projet n'a aucun rapport au niveau cohérence dans l'équipe et vue globale du projet que de participer à un petit projet avec une petite équipe. Cette expérience m'a donc beaucoup apporté sur le déroulement global d'un projet.

D'un point de vue de l'équipe que nous formions, il y avait clairement des différences entre les connaissances techniques et l'expérience de chacun. Ceci à permis d'apprendre énormément de choses quand à la conception d'un programme, mais en même temps cela a été difficile au début de comprendre comment bien architecturer les différents éléments. Heureusement que nous pouvions nous reposer certaines fois sur des autres membres du groupe qui avaient bien compris ce que l'on devait faire, et donc de leur demander conseil, voir de nous réexpliquer ce que l'on avait pas compris.

La charge de travail ayant été décidée en début de projet d'un seul coup (planification Gantt), nous ne savions même pas encore combien de temps allait prendre la découverte d'une nouvelle technologie/librairie. Pour ma part lorsque j'ai dû implémenter des fonctions utilisant la librairie 7zip-JavaBindings. Cela m'a pris un moment à comprendre comment l'utiliser mais une fois que j'avais compris, il était question de seulement quelques heures afin de finir ma partie au lieu des dizaines d'heures de planifiées. J'ai donc un temps de travail assez petit comparé aux 90h de travail à fournir théoriquement pour le projet.

De manière générale, ce projet m'a beaucoup appris, que ce soit pour la gestion d'équipe ou pour les patrons de conception.

 

Jérémy Michaud

Mon avis sur ce projet est partagé, même s'il est plutôt positif.

Tout d'abord, la différence de nos connaissances en orienté objet et en architecture logicielle a posé de nombreux problèmes. Alors que certains membres n'ont eu aucune difficulté à comprendre le fonctionnement global du projet, j'ai, pour ma part, eu beaucoup de peine à m'y faire et j'ai consacré une partie importante de mon temps à analyser et comprendre comment mes objets devaient être implémentés. Mes faibles connaissances en patron de conception (design pattern) et un manque de communication de ma part en sont les principales causes.

Ensuite, la tâche de gestion de catalogue SQLite, qui devait durer 18h, m'a posé de nombreux problèmes et a finalement demandé tout mon temps jusqu'à la fin du projet. Ces problèmes sont liés au framework que nous utilisons (ormlite) et à une erreur de conception initiale de ma part. Ce retard a reporté toute ma planification.

Cependant, bien qu'ayant consacré une grande partie du projet à résoudre des problèmes de plus en plus complexes afin que mon code fonctionne, j'en garde une très bonne expérience. En effet, je sais désormais qu'il ne faut jamais sous-estimer l'ampleur d'une tâche, même si elle semble simple au premier abord.

De plus, cela a prouvé la solidarité de notre groupe. Après avoir communiqué que cette tâche s'avérait beaucoup plus difficile qu'il n'y paraissait, les personnes ayant plus d'avance n'ont pas hésité à me proposer leur aide pour réaliser les tâches que j'avais planifiées. Cela m'a permis de me consacrer pleinement à cette tâche et je les en remercie.

Pour conclure, je trouve que ce projet m'a apporté une expérience importante sur le travail en groupe et la réalisation d'un projet bien plus conséquent que les laboratoires que nous avons fait jusqu'à maintenant. Je ne doute pas de son utilité pour mon travail de Bachelor ainsi que dans ma vie professionnelle.

 

Christophe Peretti

J'avais dès le début plusieurs attentes en ce qui concerne ce projet de programmation. Mener à bien un projet de taille conséquente, avec une équipe de développement de plus de deux personnes était pour moi une première, et je pensais bien en apprendre beaucoup. Je ne pensais malheureusement pas que nous allions nous lancer dans un projet si “compliqué” à réaliser. Le fait est que certains membres du groupes avaient déjà de grandes expériences (même professionelles) de programmation, et avaient l'habitude de travailler avec des dizaines de classes, de librairies, etc… Ce n'est malheureusement pas mon cas et j'ai eu beaucoup de difficultés à comprendre l'architecture du projet. Pour ce qui est de mon travail à proprement parler, nous avons pensé au départ mettre plusieurs membres sur des parties communes de programmation, afin d'apporter un peu d'aide dans la compréhension du code. Ainsi, nous avons travaillé avec M. Darcey sur la bibliothèque entre autres, mais lui étant souvent “bien lancé dans son code”, il n'a pas toujours été facile de me trouver des tâches à effectuer.

Au final, j'en éprouve un peu de frustration, de n'avoir pas été autant impliqué dans le projet que certains autres membres, bien que ce n'était vraiment pas la volonté qui me manquait.

Quoi qu'il en soit, je suis quand même très satisfait d'avoir pu travailler dans ce projet, au sein de ce groupe, et j'en ai plus appris sur l'organisation et la réalisation d'un projet de programmation, ainsi que les relations sociales qui en sont fortement liées, qu'importe quelle théorie que j'avais eue jusqu'à présent.

 

Samuel Darcey

Ce projet m'a beaucoup apporté pour mon expérience personnelle, notamment au niveau de ma manière de travailler en groupe mais également pour mes connaissances personelles (Structure de projet en format back-end → front-end ainsi que JavaFx). J'ai été très motivé dès le départ du projet et l'idée m'a de plus en plus plu au fur et à mesure que l'on voyait le projet avancer. Je me suis malheureusement vite heurté à des difficultés techniques qui ont fortement ralenti mon avancement et le manque d'aide ou de tacte de la part des personnes ayant plus de connaissance ont pu parfois poser préjudice.

De manière générale, il y'a eu malheureusement certains conflit au sein du goupe qui ont entacher l'expérience de travail mais je pense que nous avons réussis à surmonter la plupart des problèmes et que cela n'a pas ou peu influencé sur le rendu final du projet.

En conclusion, je tire une note positive de cette expérience et je pense qu'elle m'a apporté beaucoup du point de vue technique.