Commit or nor commit

 druide

En me promenant sur la toile, je tombe sur un plug-in chrome intéressant:
http://29a.ch/2011/10/10/swiss-address-visualization-webgl

Il s'agit d'un programme affichant les adresses répertoriées chez local.ch sous forme de points dans une map webGL. Le programmeur travaillant pour local.ch cherchait une solution intéressante pour afficher toutes ces adresses.

Partant de là, je me dis que mon navigateur, une fois la map affichée, doit posséder localement tous ces points quelque part dans le cache. Hop, un petit coup d'analyse et bingo, j'ai trouvé le fichier data.gz_ de ~40Mo.

Etape suivante, récupérer ces points depuis le fichier. L'auteur dit « The points are encoded in a Float32Array, then sorted and gziped using a python script » Donc le fichier doit être gzippé, je commence donc par le renommer en data.gz et je le donne à File Roller pour qu'il me l'ouvre. Raté, ce dernier me dit qu'il ne s'agit pas d'un fichier gzip. Bon, départ dans le shell pour voir les quelques premières lignes

$ head -c 200 data.gz_ | hd
00000000 c0 b4 6a 48 c0 b4 6a 48 00 00 e5 43 80 ee 83 48 |..jH..jH...C...H|
00000010 80 db 0f 48 00 00 cc 43 60 23 b4 48 80 9d d6 47 |...H...C`#.H...G|
00000020 00 80 d3 43 a0 20 e2 48 80 46 99 47 00 40 7f 44 |...C. .H.F.G.@.D|
00000030 40 63 e7 48 00 6d a2 47 00 80 d2 43 00 60 ea 48 |@c.H.m.G...C.`.H|
00000040 00 1c 10 48 00 00 c5 43 00 60 ea 48 00 1c 10 48 |...H...C.`.H...H|
00000050 00 00 c5 43 00 60 ea 48 00 1c 10 48 00 00 c5 43 |...C.`.H...H...C|
00000060 80 67 ed 48 00 c1 d9 47 00 80 a8 43 80 69 ed 48 |.g.H...G...C.i.H|
00000070 00 9b d9 47 00 80 a8 43 80 69 ed 48 00 9b d9 47 |...G...C.i.H...G|
00000080 00 80 a8 43 80 69 ed 48 00 9b d9 47 00 80 a8 43 |...C.i.H...G...C|
00000090 40 6f ed 48 00 d3 d9 47 00 00 a8 43 a0 6f ed 48 |@o.H...G...C.o.H|
000000a0 00 d7 d9 47 00 00 a8 43 a0 70 ed 48 80 f2 5a 48 |...G...C.p.H..ZH|
000000b0 00 00 09 44 a0 70 ed 48 80 f2 5a 48 00 00 09 44 |...D.p.H..ZH...D|
000000c0 e0 71 ed 48 80 de d9 47 |.q.H...G|
000000c8

Ok, rien de bien concluant.... Aucun magic number. Au chargement de la map, un message indique qu'il charge 3'748'071 points.


Quel est la taille du fichier?

$ ls -l data.gz_
-rw-rw-r-- 1 user user 44976852 sep 26 12:40 data.gz_


En admettant que chaque point est constitué de 3 float32 (longitude, latitude, hauteur), est-ce que ce nombre est divisible par 3 et est-ce que je retrouve mes 3'748'071 points ?


44976852 ÷ ( 3 × 4 ) = 3748071


Pour
- 44976852 taille du fichier en octets
- ( 3 × 4 ) car 3 valeurs par point en float32 (c'est l'auteur qui le dit) soit 4 octets par float

Bingo! Le fichier n'est simplement pas gzipé mais les points sont en format brut (raw). Reste plus qu'à les lire.

Un petit coup de C


#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char** argv) {
  FILE *fp;
  float *buffer;
  int i, size, nb_float;
  float x, y, h;

  /* Ouverture et lecture des datas */

  if( (fp=fopen("data.gz_", "rb")) == NULL)
  {
    fprintf(stderr, "Can't open file!\n");
    exit(EXIT_FAILURE);
  }

  fseek(fp, 0L, SEEK_END);
  size = ftell(fp);
  rewind(fp);

  fprintf(stdout, "File size %i octets\n", size);

  if(size == 0)
  {
    fprintf(stderr, "Size is 0 octets!\n");
    exit(EXIT_SUCCESS);
  }

  nb_float = size/sizeof(float);
  buffer = (float*) malloc(sizeof(float)*nb_float);

  if(buffer == NULL)
  {
    fprintf(stderr, "Unable to malloc\n");
    exit(EXIT_SUCCESS);
  }

  fprintf(stdout, "%i float lu\n", (int)fread(buffer, sizeof(float), nb_float, fp));

  for(i=0;i<nb_float;i+=3)
  {
    y = (float)*(buffer+i);
    x = (float)*(buffer+i+1);
    h = (float)*(buffer+i+2);
    fprintf(stdout, "y: %.0f x: %.0f h:%.0f\n", y, x, h);
  }

  fclose(fp);
  fp = NULL;

  free(buffer);
  buffer = NULL;

  exit(EXIT_SUCCESS);
}


Un petit coup de gcc suivi d'un test affichant les premières lignes


$ gcc -o swiss swiss.c
$ ./swiss | more
File size 44976852 octets
11244213 float lu
y: 240339 x: 240339 h:458
y: 270196 x: 147310 h:408
y: 368923 x: 109883 h:423
y: 463109 x: 78477 h:1021
y: 473882 x: 83162 h:421
y: 480000 x: 147568 h:394
y: 480000 x: 147568 h:394
y: 480000 x: 147568 h:394
y: 486204 x: 111490 h:337
y: 486220 x: 111414 h:337
y: 486220 x: 111414 h:337
y: 486220 x: 111414 h:337
y: 486266 x: 111526 h:336
y: 486269 x: 111534 h:336
y: 486277 x: 224202 h:548
y: 486277 x: 224202 h:548
y: 486287 x: 111549 h:335
y: 486291 x: 111063 h:333
y: 486343 x: 111491 h:335
y: 486343 x: 111491 h:335
y: 486404 x: 111777 h:342
y: 486426 x: 111746 h:345
y: 486426 x: 111746 h:345
y: 486426 x: 111746 h:345
y: 486426 x: 111746 h:345
y: 486426 x: 111746 h:345


Bien, maintenant, si on examine les valeurs contenues dans buffer, on se rend compte qu'il ne s'agit pas de longitude latitude WGS84 telles qu'on les trouve sur maps.google.com par exemple, mais des coordonnées Y,X,H Suisse, très certainement des CH1903.

Système de référence CH


Comment en être sûr? Hé bien en les affichant graphiquement, par exemple en utilisant openGL :)



Bien, cela semble concluant puisque ça dessine la Suisse :) Mon raisonnement semble correct !
Dans mon programme, la variation de couleur indique la hauteur. On remarque que quelques points sont hors de la Suisse, il faudra filtrer.

Reste à convertir chaque valeur en WGS84 en suivant les explications de swisstopo. Ce qui donne:

  yi = (y-600000.)/1000000.;
  xi = (x-200000.)/1000000.;
                
  lambda = 2.6779094 + 4.728982 * yi + 0.791484 * yi * xi + 0.1306 * yi * xi * xi - 0.0436 * yi * yi * yi;
  phi = 16.9023892 + 3.238272 * xi - 0.270978 * yi * yi - 0.002528 * xi * xi - 0.0447 * yi * yi * xi - 0.0140 * xi * xi * xi;
  hi = h + 49.55 - 12.60 * yi - 22.64 * xi;
                
  lon = lambda * 100. / 36.;
  lat = phi * 100. / 36.;


Voilà. On a maintenant l'intégralité des valeurs au format WGS84 et donc compatible google maps.

L'étape suivante consiste à placer toutes ces valeurs dans une base de donnée mySQL. Pour ce faire, il suffit de retourner dans le code C et d'ajouter la partie mySQL permettant la création de la base ainsi que l'insertion des enregistrements.


#include <mysql/mysql.h>
#include <mysql/errmsg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char** argv) {
  MYSQL *link;
  FILE *fp;
  float *buffer;
  char sql[255] = "";
  int i, size, nb_float;
  float x, y, h, xi, yi, hi, phi, lambda, lat, lon;

  link = mysql_init(NULL);

  if (link == NULL) {
      fprintf(stderr, "Link error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
  }

  if (mysql_real_connect(link, "localhost", "user", "********************", NULL, 0, NULL, 0) == NULL) {
      fprintf(stderr, "Connect error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
  }

  if (mysql_query(link, "DROP DATABASE IF EXISTS swissmap")) {
      fprintf(stderr, "Drop database error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
  }

  if (mysql_query(link, "CREATE DATABASE swissmap")) {
      fprintf(stderr, "CREATE error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
  }

  if (mysql_select_db(link, "swissmap")) {
      fprintf(stderr, "Select db error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
  }

  if (mysql_query(link, "CREATE TABLE map(id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT NOT NULL, swiss_y FLOAT NOT NULL, swiss_x FLOAT NOT NULL, swiss_h FLOAT NOT NULL, longitude FLOAT NOT NULL, latitude FLOAT NOT NULL, hauteur FLOAT NOT NULL)")) {
      fprintf(stderr, "CREATE table error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
  }

  /* Ouverture et lecture des datas */

  if( (fp=fopen("data.gz_", "rb")) == NULL) 
  {     
    fprintf(stderr, "Can't open file!\n");
    exit(EXIT_FAILURE);
  }     

  fseek(fp, 0L, SEEK_END);
  size = ftell(fp);
  rewind(fp);

  fprintf(stdout, "File size %i octets\n", size);

  if(size == 0) 
  {     
    fprintf(stderr, "Size is 0 octets!\n");
    exit(EXIT_SUCCESS);
  }     

  nb_float = size/sizeof(float);
  buffer = (float*) malloc(sizeof(float)*nb_float);

  if(buffer == NULL) 
  {     
    fprintf(stderr, "Unable to malloc\n");
    exit(EXIT_SUCCESS);
  }     

  fprintf(stdout, "%i float lu\n", (int)fread(buffer, sizeof(float), nb_float, fp)); 
  
  /* Parcours des datas, création de la requête d'insertion et insertion */
  for(i=0;i<nb_float;i+=3)
  {  
    y = (float)*(buffer+i);
    x = (float)*(buffer+i+1);
    h = (float)*(buffer+i+2);

    /* Conversion des coordonnées Suisse en lat/lon */
    yi = (y-600000.)/1000000.;
    xi = (x-200000.)/1000000.;

    lambda = 2.6779094 + 4.728982 * yi + 0.791484 * yi * xi + 0.1306 * yi * xi * xi - 0.0436 * yi * yi * yi;
    phi = 16.9023892 + 3.238272 * xi - 0.270978 * yi * yi - 0.002528 * xi * xi - 0.0447 * yi * yi * xi - 0.0140 * xi * xi * xi;
    hi = h + 49.55 - 12.60 * yi - 22.64 * xi;

    lon = lambda * 100. / 36.;
    lat = phi * 100. / 36.;

    memset(sql, '\0', 255);

    sprintf(sql, "INSERT INTO map VALUES(NULL,%.0f, %.0f, %.0f, %.6f, %.6f, %.2f)", y, x, h, lon, lat, hi);

    if (mysql_query(link, sql))
    {
      fprintf(stderr, "Error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
    }
  }

  fclose(fp);
  fp = NULL;

  free(buffer);
  buffer = NULL;

  mysql_close(link);

  exit(EXIT_SUCCESS);
}


Y'a plus qu'à tester... Bon, ça fonctionne mais c'est pas fulgurant! A cette vitesse-là, sur mon portable Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz 4Go de RAM, il me faudrait environ UNE SEMAINE!!!!

En cause, la configuration par défaut de mySQL et notamment la valeur « autocommit = true ». En effet, dans ce mode de fonctionnement, chaque INSERT est réellement écrit sur le disque. Il faudrait pouvoir travailler en RAM et flusher sur le disque à intervalle régulier, par exemple tous les 100'000 enregistrements. En fait, l'intervalle dépend de la RAM disponible. Si vous possédez suffisamment de RAM (14Go), vous pouvez flusher le tout une seule fois à la fin...

Pour modifier le mode de fonctionnement de mySQL, il faut utiliser les requêtes START TRANSACTION et COMMIT. START TRANSACTION désactive le mode autocommit jusqu'au prochain COMMIT. Les INSERT se font donc en RAM (très rapide) et au COMMIT, ils seront effectivement transférés sur le disque.

Ce qui donne (modification uniquement dans la boucle for):


  for(i=0;i<nb_float;i+=3)
  {
    if(i%100000 == 0)
    {
      mysql_query(link, "COMMIT");
      mysql_query(link, "START TRANSACTION");
    }
    y = (float)*(buffer+i);
    x = (float)*(buffer+i+1);
    h = (float)*(buffer+i+2);

    /*
     * y est en bas dans les cartes suisses!!!
     *
     * YMIN <= y <= YMAX soit 480000 <= y <= 850000
     *
     */

    //if(x > XMAX || x < XMIN || y > YMAX || y < YMIN) out++;

    /* Conversion des coordonnées Suisse en lat/lon */
    yi = (y-600000.)/1000000.;
    xi = (x-200000.)/1000000.;

    lambda = 2.6779094 + 4.728982 * yi + 0.791484 * yi * xi + 0.1306 * yi * xi * xi - 0.0436 * yi * yi * yi;
    phi = 16.9023892 + 3.238272 * xi - 0.270978 * yi * yi - 0.002528 * xi * xi - 0.0447 * yi * yi * xi - 0.0140 * xi * xi * xi;
    hi = h + 49.55 - 12.60 * yi - 22.64 * xi;

    lon = lambda * 100. / 36.;
    lat = phi * 100. / 36.;

    memset(sql, '\0', 255);

    sprintf(sql, "INSERT INTO map VALUES(NULL,%.0f, %.0f, %.0f, %.6f, %.6f, %.2f)", y, x, h, lon, lat, hi);
    //fprintf(stdout, "%s\n", sql);
    if (mysql_query(link, sql))
    {
      fprintf(stderr, "Error %u: %s\n", mysql_errno(link), mysql_error(link));
      exit(EXIT_FAILURE);
    }
  }
  mysql_query(link, "COMMIT");


Au premier passage de boucle, i=0 ce qui déclenche un premier passage dans le if et donc un COMMIT (dans le vide puisqu'il n'y a encore rien) et un START TRANSACTION, donc désactivation de l'autocommit. Ensuite, durant les 100'000 prochain tour, on fait des INSERT en RAM puis, lorsque i sera un multiple de 100'000 on entrera de nouveau dans le if pour faire le vrai premier COMMIT, soit 100'000 enregistrements effectifs écrits sur le disque d'un coup, puis on continue à faire les prochains 100'000 INSERT etc...

Au sortir de la boucle, on trouve un dernier COMMIT puisqu'au final, i ne sera pas multiple de 100'000 et on ne sera pas passé une dernière fois dans le if, ce qui nous ferait perdre les dernier enregistrements...

Avec ce mode de fonctionnement, on arrive à placer tous les enregistrements en moins de 15 minutes... PAS MAL.

Dernière étape ? Google peut nous proposer une coordonnée WGS84 pour une adresse donnée il peut donc aussi faire l'inverse. Il suffirait donc de parcourir chaque point de base et de demander l'adresse correspondante à google. On aurait ainsi une partie de la base de local.ch. Reste à trouver les noms se trouvant à chaque adresse...

ATTENTION cependant, l'auteur de cette application dit « the data belongs to local.ch and may not be used »

:)

Tags: mysql c autocommit swissmap

  • 8 years 3 months before
  • |