« SliderBot » : différence entre les versions

De Wikipast
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
Ligne 282 : Ligne 282 :


== Discussion ==
== Discussion ==
=== Analyse des performances ===
Temps pour calculer les json correspondants à toutes les années disponibles sur Wikipast (de 1765 à 1999) : 1 minute 51 secondes.
Temps moyen par année : 0.47 secondes.


== Code Complet ==
== Code Complet ==

Version du 15 mai 2018 à 11:56

Objectifs

  • Crée une base de données en associant [Personne, Date, Lieu] via un scrapping de page.
  • Crée un ensemble de cartes affichant les positions de toutes les personnes recensées pour chaque date (discretisation par année ?).
  • Création d’un slider dynamique, où l’annee souhaitée est choisie, et affiche la carte correspondante (JavaScript ou HTML)

Description

Le bot extrait les données nécessaires de Wikipast, en parcourant chaque page existante d'années à travers le temps. Les informations retenues sont d'abord les mentions de villes et deuxièmement les mentions de personnes respectives pour chaque année. Ce travail est fait par un code python, qui va sauvegarder l'information dans un tableau. Ce tableau est utilisé pour afficher les lieux mentionnées et les personnes respectives sur une carte, pour une année spécifique, en utilisant un code JavaScript.

Le but final est d'appliquer cette méthode pour chaque année existante dans la base de données de Wikipast et ainsi de créer une carte interactive permettant de naviguer à travers le temps et l'espace et ainsi visualiser la distribution d'événements sur le globe au fil des ans.

Fonctionnement

Server

Node.js est une plateforme logicielle libre en JavaScript orientée vers les applications réseau qui doivent pouvoir monter en charge. Parmi les modules natifs de Node.js, on retrouve HTTP qui permet le développement de serveurs HTTP.

Username Password
SliderBot SliderBot123

server.js

Le serveur est initialisé à l'aide du module 'express' de Node.js, pour générer le cadre d'une application. La méthode app.post() indique au serveur qu'il est en attente d'une demande HTTP. La méthode app.listen() définie le chemin sur lequel le serveur est prêt à l'écoute.

const express = require('express');
const app = express();

app.post('/', function(req, res){
    console.log('Post request received: ');
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    req.on('data', function (chunk) {
        var buffer = JSON.stringify(chunk);
        var data = getData(buffer); 
        console.log('GOT DATA : '+data);       
    });
    res.end(JSON.stringify({"data":0}));
});

app.listen(3000, () => {
    console.log('Server listening on port 3000!');
});

communication.js

JSON (JavaScript Object Notation) est un format de données textuelles dérivé de la notation des objets du langage JavaScript. La fonction sendReq() envoie une demande au server pour recevoir l'information sous ce format.

var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
var IP_ADDRESSE = "128.179.178.60";
var PORT = "3000";

function sendReq(){
    var request = new XMLHttpRequest();
    request.open('POST', 'http://'+IP_ADDRESSE+':'+PORT, true);
    request.setRequestHeader("Content-type", "application/json");
    request.onload = () => {
      if (request.status >= 200 && request.status < 400) {
        const res = JSON.parse(request.responseText);
        console.log(res);
      } else {
        console.log("Error on server side.")
      }
    };
    request.onerror = () => {
        console.log("Error on communication.");
    };
    request.send(JSON.stringify({"year":1996}));
}   

sendReq();

Extraction de données

BeautifulSoup est une bibliothèque de parsage (analyse syntaxique) en code Python pour le langage HTML/XML. Dans ce cas, elle est utilisée pour extraire des données de Wikipast : villes et personnes mentionnées, dans les pages existantes concernant les années. Geopy est une bibliothèque Python, utilisée pour localiser les coordonnées d'endroits spécifiques dans le monde (dans ce cas des villes).

Code Complet: maincode.py [1]

Récupération des lieux et personnes

Le résultat du parsing, avec les services de BeautifulSoup, est une liste de tuples: année, lieux, personne. Ceci est principalement fait par la fonction getYearCityAndName:

def getYearCityAndName(div):
        tagDate = div[div.index("<a"):div.index("</a>")]
        date = tagDate[tagDate.index(">") + 1:]
        year = date.split(".")[0]
        if not year.isnumeric():
            return
        if div[div.index("</a>") + 7] == '-' or div[div.index("</a>") + 4] == '.' or div[div.index("</a>") + 7] == ' ':
            return
        else:
            skipDate = div[div.index("<a")+ 2:]
            tagCityStart = skipDate[skipDate.index("<a"):]
            tagCity = tagCityStart[:tagCityStart.index("</a>")]
            city = tagCity[tagCity.index(">") + 1:]
            skipCity = tagCityStart[tagCityStart.index("</a>")+6:tagCityStart.index("</li>")]
            class MyHTMLParser(HTMLParser):
                data = []
                def handle_starttag(self, tag, attrs):
                    if tag == 'a':
                        for attr in attrs:
                            if str(attr[0]) == 'title':
                                if isName(attr[1]):
                                    self.data.append(attr[1])
            parser = MyHTMLParser()
            parser.feed(skipCity)
            if len(parser.data) != 0:
                return (year, city, parser.data)

Localisation

Pays vs. Capitale

Si le lieu d'un événement sur une page Wikipast est décrite seulement par son pays, plutôt que de l'afficher au centre géographique du pays, l'événement sera plutôt affiché comme s'étant produit dans la capitale de ce pays. Ceci est fait à l'aide d'un fichier countries.py [2] qui contient une liste avec les pays dans le monde et leur capitale, par la fonction capitalIfCountry:

def capitalIfCountry(location):
"""
If location is a country, then it changes it to its capital
"""
   dataPays = next((item for item in countries if item["name"] == location), False)
   if(dataPays):
      return (dataPays['capital'])
   else:
      return location
Conversion de coordonnées

La location est convertie en coordonnées, avec les services de geo-localisation de Geopy, par la fonction finalNameCoordTuple:

def finalNameCoordTuple(tuples):
"""
Produces an array 'output' containing [Name, latitude, longitude]
If location is a country, then it changes it to its capital
"""
   output = []
   for tuple in tuples:
      coord = geolocator.geocode(capitalIfCountry(tuple[1]))
      output.append([tuple[2], coord.latitude, coord.longitude])
   return output

Ici, les différents éléments du tuple proviennent du parsing : tuple[1] correspond au nom du lieu et tuple[2] correspond à la personne.

Répulsion de lieux identiques

L'affichage correct de plusieurs événements se déroulant au même lieu. En effet, geocode renvoie une position (latitude, longitude) précisément identique pour chaque ville. Lorsque deux événements se passent au même lieu, les points se confondent rigoureusement et l'API Google ne permet pas de les séparer (peu importe le niveau de zoom).

Pour parer à cette éventualité, la fonction repulsePoints décale sur la carte de quelques centaines de mètres les événements (par un simple incrément arbitraire de longitude et latitude).

def repulsePoints(tuples):
        for i in range (len(tuples)):
            for j in range (len(tuples)):
                if (i != j and tuples[i][1] == tuples[j][1] and tuples[i][2] == tuples[j][2]):
                    tuples[j][1] = tuples[i][1] + 0.005
                    tuples[j][2] = tuples[i][2] + 0.005
        return tuples

Cela étant dit, une telle méthode peut donner l'impression d'une précision artificielle sur les lieux (qu'il sera impossible d'éviter, les lieux sur Wikipast étant au mieux localisés par ville).

Création d'un fichier Json

Finalement le fichier JSON est généré, et en prenant en paramètres des arrays en format [nom, latitude, longitude] et l'année souhaitée (commune à toutes les fonctions du programme), produitun fichier de la forme (par exemple ici 1986.json, où l'on a gardé qu'un événement), qui est enregistré dans un dossier server/maps sur github.

[
    {
       "proprietes" : ["Enzo Ferrari",44.5255217,10.8663607]
    }
]

La fonction est la suivante:

def createJson(inputData, year):
"""
Takes array [Name, lat, long] for each year
Outputs in .json such that it can be plotted
"""
    indent = '    '
    indent2 = '      '
    maxLimit = len(inputData)
    with open(str(year)+'.json', 'w') as outfile:
        outfile.write('['+'\n')
        for i in range (maxLimit):
            if i == (maxLimit-1):
                outfile.write(indent + '{'+'\n')
                outfile.write(indent2 + ' "proprietes" : '+'['+'"'+remove_accents(str(inputData[i][0]))+'"'+ ','+ str(inputData[i][1])+ ','+ str(inputData[i][2])+']'+'\n')
                outfile.write(indent + '}'+'\n')
             else:
                outfile.write(indent + '{'+'\n')
                outfile.write(indent2 + ' "proprietes" : '+'['+'"'+remove_accents(str(inputData[i][0]))+'"'+ ','+ str(inputData[i][1])+ ','+ str(inputData[i][2])+']'+'\n')
                outfile.write(indent + '}'+','+'\n')
        outfile.write(']')
    outfile.close()

Tous les besoins précis de structure d'un fichier Json sont pour l'instant (faute de mieux) gérés manuellement (comme la conversion en str des float de coordonnées, ou bien les retours à la ligne).

Placement sur la carte

Le placement des points (personne, ville) est fait avec un code en language HTML (HyperText Markup Language), à partir du fichier JSON crée auparavant. Les points respectives à chaque année sont affichés sur une carte Google Maps. Un slider est aussi crée pour pouvoir naviguer dans le temps.

Code complet: index.html [3]

Le code est caractérisé par trois fonctions principalement:

initCarte() initialise la carte avec les caractéristiques définies: sa position par default, l'option zoom et le slider, prédéfini a une certaine année.

 function initCarte(){
    var slider = document.getElementById("slider_annee");
    var output = document.getElementById("annee");
    var b_autoplay = document.getElementById("autoplay");
    output.innerHTML = slider.value;
    slider.onmouseup = function() {
      update();
    }
    slider.oninput = function() {
      output.innerHTML = this.value;
    }
    var options={
      zoom:2,
      center:{lat:30,lng:20}
    }
    update();
    // ...

update() extrait tous les points du fichier JSON pour l'année définie par le slider, ceci fait à travers un liens direct vers github.

    function update(){
      markers=[];
      var carte=new google.maps.Map(document.getElementById('carte'), options);
      var requestURL = 'https://raw.githubusercontent.com/EtienneBonvin/SliderBot/master/server/maps/'+String(slider.value)+'.json';
      var request = new XMLHttpRequest()
      request.open('GET', requestURL);
      request.responseType = 'json';
      request.send();
      request.onload = function() {
        var reponse = request.response;
        for(var i=0; i<reponse.length; i++){
          ajouterPoint(reponse[i]);
        }
        var regroupement = new MarkerClusterer(carte, markers,{
          imagePath: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'
        });
      }
    }

ajoutePoints() affiche les points sur la carte, qui ont été auparavant regroupés, par les coordonnées spécifiées (latitude, longitude).

function ajouterPoint(proprietes){
        var point=new google.maps.Marker({
          position:{lat:proprietes["proprietes"][1], lng: proprietes["proprietes"][2]},
          map:carte,
        });
        var fenetreInfo=new google.maps.InfoWindow({
          content:proprietes["proprietes"][0]
        });
        point.addListener('click', function(){
          fenetreInfo.open(carte,point);
        });
        markers.push(point);
    }

Exemple

À gauche est un exemple de comment la carte du monde est visualisé pour l'année 1809. Dans le bas de la carte il y a le slider pour avancer ou retourner en arrière dans le temps. La carte contient l'option de naviguer dans l'espace et de zoom, ainsi que des boutons de localization.

À droite est un exemple de l'année 1962, dont la carte est zoom-in sur Genève. Quand le zoom est assez grand pour identifier des points individuellement, les noms de personnes et les details de l'événement sont affichés.

Discussion

Analyse des performances

Temps pour calculer les json correspondants à toutes les années disponibles sur Wikipast (de 1765 à 1999) : 1 minute 51 secondes. Temps moyen par année : 0.47 secondes.

Code Complet

https://github.com/EtienneBonvin/SliderBot

Groupe

Nom Pseudo
Paul Guhennec Pguhennec
Maël Wildi mwildi
Etienne Bonvin AbsInt
Mathilde Raynal PizzaWhisperer
Stefano Politi spoliti