Skip to Content

QGIS, le cas de l'extension Sextante sur Mac OS X ou comment la connaissance de Python permet de résoudre les problèmes, sans uniquement se lamenter...


Il y a quelque temps sortait l'extension Sextante pour Quantum GIS et cet évènement a créé une grande excitation chez les Sigistes. Il y a de quoi, ce plug-in écrit en Java a une longue histoire résumée dans Librairie Sextante: point de la situation sur le ForumSIG. Il apporte, en pratique, plusieurs centaines d'algorithmes de géotraitement aux logiciels SIG Java comme gvSIG, OpenJump, Kosmo ou Udig et continue d'évoluer (sextantegis.blogspot.be/).

La version pour QGIS est écrite en Python et tout le monde a congratulé Victor Olaya, avec raison. Le seul problème est qu'il est loisible de constater, une nouvelle fois, que les utilisateurs de Mac OS X sont les grands oubliés, tout pour Linux et Windows. En pratique rien ne marchait, ni les modules GRASS, ni ceux de R et à fortiori ceux pour SAGA GIS, ce qui est normal puisque cette application n'a pas été portée sur Mac.

Démarches pour tenter de résoudre les problèmes rencontrés sur MAC OS X

Dans la philosophie des logiciels libres, nous avons donc décidé (moi + d'autres utilisateurs de Mac OS X sur la liste User de QGIS) d'analyser les problèmes pour tenter de les résoudre:

  • la première tentative a été d'essayer de compiler SAGA GIS suivant les directives de http://sourceforge.net/apps/trac/saga-gis/wik/Compiling%20SAGA%20on%20Mac%20OS%20X , avec peu de succès, et surtout avec la peur de modifier certaines librairies indispensables déjà présentes sur Mac OS X et dont la modification pourrait perturber le fonctionnement d'autres programmes;
  • connaissant très bien Python, je me suis attaqué aux problèmes du non-fonctionnement des programmes présents sur Mac comme GRASS GIS et R;
  • les solutions fournies ici ont d'abord été soumises aux autres utilisateurs impliqués qui les ont testées avec succès;
  • avant de montrer la résolution du problème, il est nécessaire de détailler comment fonctionne l'extension Sextante;

Comment fonctionne l'extension Sextante ?

Quelque soit le logiciel choisi Sextante utilise le module Python subprocess pour appeler directement le logiciel et non des modules spécialisés comme Rpy2 ou grass.script. L'utilisation du module subprocess a déjà été illustrée sur le Portail, notamment avec R, dans QGIS : lancer des scripts Python ou des commandes Shell depuis la Console Python ou avec Script Runner (sans créer d'extension). L'appel de ce module est réalisé dans deux scripts nommés respectivement:

  • RUtils.py (dans ~/.qgis/python/plugins/sextante/r/RUtils.py):

  •   et GrassUtils.py (dans ~/.qgis/python/plugins/sextante/grass/GrassUtils.py):

  • c'est le même principe avec les autres modules non écrits en Python (GdalUtils.py et SagaUtils.py).

Au premier chargement de l'extension dans QGIS:

  • Au premier lancement, Sextante va créer un dossier de traitement des données nommé sextante  dans /Users/martinlaloux/sextante (chez moi). C'est dans ce dossier que vont être placés tous les fichiers temporaires pour les traitements (la signification de ces fichiers et dossiers est expliquée dans la suite):

  • ensuite, les scripts du module sextante créent classiquement les interfaces avec PyQt qui s'affichent dans QGIS.

Il est alors possible de lancer un des algorithmes de traitement:

Lancement d'un algorithme R: processus de RUtils.py

L'algorithme est choisi dans l'interface graphique:

Après l'utilisation de l'interface de l'algorithme, c'est le fichier RUtils.py qui est le moteur du traitement avec la classe  RUtils (les fonctions sont représentées par les cubes roses):

  • RUtils va d'abord créer un fichier temporaire nommé sextante_script.r dans le dossier sextante grâce à la fonction createRScriptFromRCommands(commands) (ligne 32 de RUtils.py);
  • le contenu de sextante_script.r (c'est un script R qui va effectuer le traitement demandé):
library("rgdal")
polyg = readOGR("/Users/Shared/chemin.shp",layer="chemin")
numpoints=10
pts=spsample(polyg,numpoints,type="regular")
output=SpatialPointsDataFrame(pts, as.data.frame(pts))
writeOGR(output,"/Users/martinlaloux/sextante/tempdata/rcreateregularsamplinggrid6.shp","rcreateregularsamplinggrid6", driver="ESRI Shapefile")
  • la couche de départ, chemin.shp est le fichier shapefile original et non la couche QGIS, et la couche résultante sera un fichier shapefile (par défaut ici) placé dans le dossier de traitement, rcreateregularsamplinggrid6.shp;
  • ce script R temporaire est ensuite exécuté dans la fonction  executeRAlgorithm(alg) avec le module subprocess (ligne 50 de RUtils.py):
command = ["R", "CMD","BATCH", "--vanilla", RUtils.getRScriptFilename(), RUtils.getConsoleOutputFilename()]
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True)
  • la couche résultante est chargée dans QGIS.

Lancement d'un algorithme Grass: processus de GrassUtils.py

L'algorithme est toujours choisi dans l'interface graphique:

Après l'utilisation de l'interface de  l'algorithme, c'est le fichier GrassUtils.py qui est le moteur du traitement avec la classe GrassUtils:

  • GrassUtils va d'abord créer un secteur GRASS (LOCATION) temporaire, nommé temp_location avec deux jeux de données (MAPSET) nommés  PERMANENT et user dans /Users/martinlaloux/sextante/tempdata/grassdata/temp_location/ grâce à la fonction createTempMapset() (ligne 154 de GrassUtils.py):
    • ce secteur peut être de 2 types suivant les données de départ, x-y (cartésien) ou longitude - latitude, suivant les données et la région est ajustée automatiquement en fonction de la couche choisie;
  • la fonction createGrassBatchJobFileFromGrassCommands(commands) (ligne 139) crée ensuite un fichier temporaire de traitement (réalisé avec createGrassScript(commands) à la ligne 88) nommé grass.script.sh (ou .bat pour Windows qui utilise, en plus les fonctions grassWinShell() et writeGrassWindow(filename)):
g.region n=142872.076258 s=142627.598398 e=248857.288594 w=248425.188189 res=1.0
v.in.ogr min_area=-1 dsn="/Users/Shared" layer=chemin output=tmp1342039013594 --overwrite -o
v.buffer input=tmp1342039013594 distance=20 tolerance=0.01 output=output --overwrite
v.out.ogr -ce input=output dsn="/Users/martinlaloux/sextante/tempdata" format=ESRI_Shapefile olayer=grassvbufferdistance2 type=auto
exit
  • ce script temporaire est ensuite exécuté dans GRASS GIS avec la fonction executeGrass(commands, progress) (ligne 237):
command = "grass64 " + GrassUtils.grassMapsetFolder() + "/user"
....
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True).stdout
#....suite du traitement....
  • la couche résultante est exportée en un fichier shapefile (v.out.ogr) puis chargée dans QGIS.

Autres traitements

Les autres modules non écrits en Python fonctionnent de la même manière:

À la fin du traitement:

  • en théorie, les fichiers shapefiles résultants sont chargés dans QGIS (et conservés dans le dossier de travail sextante);
  • le résultat des traitements est écrit dans le fichier sextante_qgis.log situé aussi dans ce dossier de traitement;
  • force est de constater qu'avec la configuration d'origine rien ne marche sur Mac OS X avec, dans ce fichier, des indications du genre:

ERROR|...
....  
ERROR|Sun Apr 01 2012 21:22:27|[Errno 4] Interrupted system call

  • l'examen des scripts montre bien que le cas de Mac OS X n'est jamais pris en compte, les systèmes considérés sont exclusivement Windows et Linux (dans toutes les expressions conditionnelles).

Solutions

Il y a plusieurs manières de tenter de résoudre les erreurs dans l'extension:

  • par l'examen des erreurs dans le fichier sextante_qgis.log;
  • par l'examen du code Python;
  • par la combinaison des deux démarches.

1) par l'examen de l'erreur signalée dans sextante_qgis.log: le cas de GRASS

  • avec l'application d'un algorithme de GRASS, l'erreur signalée dans le fichier log était:

ALGORITHM|Sun Jul 08 2012 12:35:08|Sextante.runalg("grass:v.buffer.distance","/Volumes/NO NAME/fmidi/point3D.shp","29","0.01",False,False,None)
INFO|Sun Jul 08 2012 12:35:08|GRASS execution commands|g.region n=119662.846231 s=117389.474738 e=136624.848399 w=115154.158713 res=1.0|v.in.ogr min_area=-1 dsn="/Volumes/NO NAME/fmidi" layer=point3D output=tmp1341743708714 --overwrite -o|v.buffer input=tmp1341743708714 distance=29 tolerance=0.01 output=output --overwrite|v.out.ogr -ce input=output dsn="/Users/martinlaloux/sextante/tempdata" format=ESRI_Shapefile olayer=grassvbufferdistance2 type=auto
INFO|Sun Jul 08 2012 12:35:08|GRASS execution console output|/bin/sh: grass64: command not found

C'est donc, à la ligne 248 du script GrassUtils.py, l'expression:

command = "grass64 " + GrassUtils.grassMapsetFolder() + "/user"

qui est ici en cause dans son utilisation avec le module subprocess (ligne 251):

proc = subprocess.Popen(command,...)

Si je détaille ce que fait le module subprocess, il demande à grass64 d'exécuter le script grass.script.sh dans le secteur temp_location et le jeu de données user or l'exécutable grass64 n'existe pas à priori sur Mac OS X dans /usr/bin ou /usr/local/bin -> erreur.

Mais où se trouve l'équivalent de grass64 avec l'application GRASS-6.4.app de William Kyngesburye (www.kyngchaos.com/software/grass). L'application est en fait un paquet (bundle en anglais), qui contient l’exécutable et les ressources nécessaires pour l’exécutable. C'est donc dans ce dossier que doit se trouver l'exécutable.

  • contenu du paquet GRASS-6.4.app (clic droit de la souris ou Ctrl+click puis « afficher le contenu du paquet »):

  • l'exécutable est ici  grass.sh (vous pouvez le tester en ouvrant le terminal dans ce dossier et en tapant ./grass.sh).

La solution est donc de remplacer « grass64 » par « /Applications/GRASS-6.4.app/Contents/MacOS/grass.sh » dans la ligne 248 de GrassUtils.py

command = "/Applications/GRASS-6.4.app/Contents/MacOS/grass.sh" + GrassUtils.grassMapsetFolder() + "/user"
....
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True).stdout
#....suite du traitement....

Avec comme résultat dans le fichier log:

|Batch job '/Users/martinlaloux/sextante/grass_batch_job.sh' (defined in GRASS_BATCH_JOB variable) was executed.|Goodbye from GRASS GIS

Le problème du module GRASS dans Mac OS X est donc réglé.

2) par l'examen du code Python: le cas de R

Le cas de R est un peu plus subtil, car si au départ les algorithmes ne marchent pas,  aucune indication de la cause n'est fournie dans le fichier log, hormis:

 ERROR|Sun Apr 01 2012 10:13:03|[Errno 4] Interrupted system call

Lors de l'appel de l'algorithme, le fichier temporaire sextante_script.r est bien créé et donc l'erreur provient donc une nouvelle fois de l'appel du module subprocess aux lignes 58 et 59 de RUtils.py:

command = ["R", "CMD", "BATCH", "--vanilla", RUtils.getRScriptFilename(), RUtils.getConsoleOutputFilename()]
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT,universal_newlines=True)

Comme le fichier temporaire contient tout ce qui est nécessaire à l'exécution du script sans QGIS (voir contenu plus haut), il est possible de l'utiliser dans le Shell Python pour examiner ce qui se passe:

>>> import subprocess
>>> # solution 1 qui marche
>>> proc=subprocess.Popen(['R', 'CMD', 'BATCH', 'sextante_script.r'],stdout=subprocess.PIPE)
>>> # solution 2 qui marche aussi
>>> proc=subprocess.Popen(['R', 'CMD', 'BATCH', 'sextante_script.r'],stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True)
>>> # solution 3 qui ne marche pas et crée l'erreur
>>> proc=subprocess.Popen(['R', 'CMD', 'BATCH', 'sextante_script.r'], shell= True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True)

et il est facile de constater que l'erreur provient du paramètre « shell=True » dans la commande subprocess.

Et là je me suis rendu compte que c'est une erreur que j'ai maintes fois commise et qui est résumée dans stackoverflow.com/questions/9095733/getting-output-from-python-script-in-python-tests/9096104#9096104:

"If you're using shell=True, don't pass the program and its arguments as a list
....
On Unix, with shell=True: If args is a string, it specifies the command string to execute through the shell. This means that the string must be formatted exactly as it would be when typed at the shell prompt. This includes, for example, quoting or backslash escaping filenames with spaces in them. If args is a sequence, the first item specifies the command string, and any additional items will be treated as additional arguments to the shell itself.
"

Mac OS X étant un Unix, c'est de là que provient l'erreur. La solution est donc de remplacer la ligne 58 par:

  • command = "R CMD BATCH --vanilla " + RUtils.getRScriptFilename() + " "+ RUtils.getConsoleOutputFilename() dont le résultat est une chaîne de caractère qui contient la commande exacte à effectuer.

ou, si l'on veut garder la formulation exacte de la ligne 58, sans le « shell=True » dans:

  • proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True)

et le script devient dans le premier cas:

       command = "R CMD BATCH --vanilla " + RUtils.getRScriptFilename() + " "+ RUtils.getConsoleOutputFilename()
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True)
proc.wait()

Le problème du module R dans Mac OS X est donc réglé.

Résultat final des corrections pour Mac OS X

Maintenant, tout fonctionne:

Conclusions finales

Comme tout bon utilisateur de logiciel libre, j'ai donc fourni mes solutions à mes partenaires de la liste User, puis après vérification de leur part, sur la liste User de QGIS:

et à la demande des développeurs, la solution a été fournie sur le « bug tracker » de l'extension Sextante ( ticket on sextante redmine instance).

en attendant une nouvelle version où ces éléments seront pris en compte (la solution du problème avec R, plus générale, a déjà été corrigée par Victor Olaya sur la dernière version de développement, voir hub.qgis.org/issues/6029 ). Seul subsiste le problème de SAGA GIS.....

Mais il est aussi nécessaire de constater que c'est la connaissance préalable de Python et du système Mac OS X qui m'a permis de résoudre le problème. Les développeurs de logiciels libres ne peuvent pas tout connaître, surtout avec les divers systèmes d'exploitation existants, et les aides seront toujours nécessaires si l'on veut que tout marche partout. Se lamenter sans rien faire ne sert à rien !

La connaissance de Python devient de plus en plus indispensable en sachant qu'en plus de QGIS, GRASS GIS, ArcGIS ou gvSIG (via Jython) utilisent Python (voir Python: géospatial, dialectes (standard, pour ESRI, pour FME, pour GvSIG etc.) et incompréhensions...)

En résumé:

  • pour que le module GRASS marche sur Mac OS X, il faut remplace  la ligne de GrassUtils.py:

command = "grass64 " + GrassUtils.grassMapsetFolder() + "/user"

  • par

command = "/Applications/GRASS-6.4.app/Contents/MacOS/grass.sh" + GrassUtils.grassMapsetFolder() + "/user"

  • pour que le module R fonctionne sur Mac OS X, il faut remplacer la ligne de RUtils.py:

command = ["R", "CMD", "BATCH", "--vanilla", RUtils.getRScriptFilename(), RUtils.getConsoleOutputFilename()]

  • par

command = "R CMD BATCH --vanilla " + RUtils.getRScriptFilename() + " "+ RUtils.getConsoleOutputFilename()

  • ou modifier,  en supprimant « shell=True » de la ligne:

proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)

Tous les traitements ont été effectués sur Mac OS X  avec QGIS versions 1.7.4, 1.8  et 1.9 dev. , Sextante version 1.0.7 et Python 2.6.


Site officiel : Quantum GIS (QGIS)
Site officiel : extension Sextante pour QGIS
Autres Liens : Librairie Sextante: point de la situation
Autres Liens : QGIS : lancer des scripts Python ou des commandes Shell depuis la Console Python ou avec Script Runner (sans créer d'extension)
Autres Liens : Python: géospatial, dialectes (standard, pour ESRI, pour FME, pour GvSIG etc.) et incompréhensions...


Creative Commons License
licence Creative Commons Paternité-Pas d'Utilisation Commerciale-Partage des Conditions Initiales à l'Identique Commerciale 2.0 France

Commentaires

la solution complète pour utiliser GRASS avec tous les systèmes.

La fonction complète pour utiliser le script pour Windows, Linux et Mac OS X est:

def executeGrass(commands, progress):
        if SextanteUtils.isWindows():
            GrassUtils.createGrassScript(commands)
            command = ["cmd.exe", "/C ", GrassUtils.grassScriptFilename()]
        else:
            gisrc =  SextanteUtils.userFolder() + os.sep + "sextante.gisrc"
            os.putenv("GISRC", gisrc)
            os.putenv("GRASS_MESSAGE_FORMAT", "gui")
            os.putenv("GRASS_BATCH_JOB", GrassUtils.grassBatchJobFilename())
            GrassUtils.createGrassBatchJobFileFromGrassCommands(commands)
            os.chmod(GrassUtils.grassBatchJobFilename(), stat.S_IEXEC | stat.S_IREAD | stat.S_IWRITE)
            if SextanteUtils.isMac():         
                command = "/Applications/GRASS-6.4.app/Contents/MacOS/grass.sh " + GrassUtils.grassMapsetFolder() + "/user"
            else:
                command = "grass64 " + GrassUtils.grassMapsetFolder() + "/user"
        loglines = []
        loglines.append("GRASS execution console output")
        proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True).stdout
        for line in iter(proc.readline, ""):
            if "GRASS_INFO_PERCENT" in line:
                try:
                    progress.setPercentage(int(line[len("GRASS_INFO_PERCENT")+ 2:]))
                except:
                    pass
            else:
                loglines.append(line)
        SextanteLog.addToLog(SextanteLog.LOG_INFO, loglines)
        shutil.rmtree(GrassUtils.grassMapsetFolder(), True)

et si vous voulez avoir l'aide dans les dialogues (help), placez

GRASS_HELP_FOLDER=/Applications/GRASS-6.4.app/Contents/MacOS/docs/html

dans le fichier sextante_qgis.conf  (/Users/martinlaloux/sextante/sextante_qgis.conf, dans mon cas).

Bravo Martin

Bravo Martin et merci pour cet article, outre l'intérêt pour les utilisateurs Mac OS, je trouve très intéressant l'explication du fonctionnement du module, cela permet de voir d'autres perspectives et utilisations.

Les 2 problèmes ont été

Les 2 problèmes ont été réglés par Victor Olaya

Pour GRASS GIS (en tenant compte des personnes qui auraient installé GRASS GIS par Fink, MacPorts ou Homebrew)

hub.qgis.org/issues/6015

 

Pour R:

hub.qgis.org/issues/6029

 

Poster un nouveau commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement.