User:Melnibon/FR:GarminCustomMaps

From OpenStreetMap Wiki
Jump to navigation Jump to search

Cartes personnelles (Custom Maps) pour les Garmin Oregon

Les nouveaux Garmin sont capables d'utiliser une image JPG géolocalisée en tant que fond de carte, par contre sans routage. Consulter ce lien http://www.garmin.com/garmin/cms/site/us/onthetrail/custommaps. Le principe est de partir d'une image jpg, de la géolocaliser dans Google Earth puis la sauver au format kmz directement exploitable par ces merveilleux Oregon.

Il y a quelques sites qui permettent de récupérer des fonds de cartes officiels, par exemple http://www.skitour.fr/maps/ign/gpx-online.php. La tentation est grande de vouloir faire une mosaïque avec plusieurs copies d'écran pour faire une carte complète. Le problème, à part l'absence d'autorisation pour le faire, est que la carte est généralement trop grande pour l'Oregon. Il ne faut pas dépasser 1024x1024 pixels, sinon l'unité réduit la résolution en floutant l'image. J'ai développé un script python pour découper automatiquement les fichiers kmz de manière à ne pas dépasser 1024 pixels dans chaque dimension. Au lieu de ne stocker qu'un seul fichier kmz, trop gros, sur le Garmin, on stocke plusieurs petits fichiers qui sont affichés parfaitement, comme sur l'écran du navigateur.

Voici la démarche qu'on pourrait employer en théorie :

  1. lancer Firefox sur le lien cité plus haut et The Gimp
  2. choisir une zone sur la carte
  3. copie écran à enregistrer dans le presse-papier (touche impr écran chez moi)
  4. dans The Gimp menu édition, coller en tant que nouvelle image
  5. outil de découpage (le cutter) pour ne garder que la partie utile de l'image (pas les logos ni les bordures)
  6. choisir une autre zone voisine sur la cartes, les deux doivent avoir une partie commune suffisamment grande (10%)
  7. copie écran dans le presse-papier
  8. dans The Gimp, coller en tant que nouveau calque
  9. outil cutter, découper la partie utile de l'image en ne traitant que ce calque (voir les options de l'outil)
  10. afficher la pile des calques et mettre celui-ci en mode différence
  11. outil déplacement de calque, déplacer le calque jusqu'à ce que la partie commune devienne entièrement noire
  12. remettre le calque en mode normal et fusionner tous les calques
  13. adapter la taille de l'image à celle des calques visibles
  14. recommencer au niveau du point 6

Pour finir :

  1. découper une dernière fois l'ensemble des calques pour obtenir une image rectangulaire sans aucun trou
  2. aplatir et sauver au format jpg, qualité 95 par exemple

On obtient une très grande image. Si la zone n'est pas trop grande, il n'y a pas de distorsion des bords (euh, franchement, on trouve à vendre des cartes officielles avec des collages de zones qui n'ont pas la même allure et qui ne sont pas jointives, sans compter qu'elles sont complètement obsolètes).

Il reste à la géolocaliser, c'est à dire indiquer les coordonnées exactes des quatre coins. Pour ça, on ouvre Google Earth, on zoome sur la zone correspondant à l'image, on ajoute une nouvelle superposition d'image et on ajuste la position de cette image pour avoir une superposition parfaite. A la fin, on enregistre la superposition en tant que fichier kmz

Seulement voila, ça ne passe pas bien dans le Garmin, l'image est assez floue, d'autant plus que l'image est grande. Voici un script python qui sectionne un fichier kmz en autant qu'il faut pour que la taille de chaque partie ne dépasse pas 1024 pixels dans aucune dimension. On obtient plusieurs fichiers kmz qui constituent une partition du fichier d'origine. Ils sont nommés avec un suffixe N,S,W,E selon la zone qu'ils représentent. Attention, NNNW signifie que l'image a été coupée 3 fois verticalement et une fois horizontalement, ce n'est pas un azimut mais une sorte de code.

#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) Pierre Nerzic 02/2010, pierre.nerzic@free.fr

# script python pour couper une carte kmz en morceaux de moins de 1024x1024 pixels

import zipfile
import xml.dom.minidom
import Image
import StringIO
import os
import os.path


def TraiterImage(suffixe, nomfich, image, name,href,north,south,east,west):
    """ si l'image est trop grande, il y a un appel récursif sur les deux moitiés de l'image
        sinon, l'image est ajoutée dans un nouveau fichier kmz"""
    # test sur la taille
    larg,haut = image.size
    if haut > 1024:
        # coupure verticale et appels récursifs
        TraiterImage(suffixe+'N', nomfich, image.crop((0,0,larg,haut/2)), name,href,north,(north+south)/2,east,west)
        TraiterImage(suffixe+'S', nomfich, image.crop((0,haut/2+1,larg,haut)), name,href,(north+south)/2,south,east,west)
    elif larg > 1024:
        # coupure horizontale et appels récursifs
        TraiterImage(suffixe+'W', nomfich, image.crop((0,0,larg/2,haut)), name,href,north,south,(east+west)/2,west)
        TraiterImage(suffixe+'E', nomfich, image.crop((larg/2+1,0,larg,haut)), name,href,north,south,east,(east+west)/2)
    else:
        # si le suffixe est vide, on ne fait rien, car l'image est de la bonne taille
        if suffixe == '': return
        
        # l'image est de la bonne taille, on crée un nouveau kmz uniquement pour elle
        kmz = zipfile.ZipFile(nomfich+'-'+suffixe+'.kmz','w')

        # enregistrer l'image dans files/
        nomimg = 'files/'+suffixe+'-'+href
        fich = StringIO.StringIO()
        image.save(fich, 'JPEG', quality=95)
        kmz.writestr(nomimg, fich.getvalue())
        fich.close()

        # créer un document xml appelé doc.kml dans ce fichier zip
        doc = xml.dom.minidom.parseString('<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://earth.google.com/kml/2.2"/>')
        GroundOverlay = doc.getElementsByTagName('kml')[0].appendChild(doc.createElement('GroundOverlay'))
        GroundOverlay.appendChild(doc.createElement('name')).appendChild(doc.createTextNode(name+'-'+suffixe))
        GroundOverlay.appendChild(doc.createElement('color')).appendChild(doc.createTextNode('e3ffffff'))
        Icon = GroundOverlay.appendChild(doc.createElement('Icon'))
        Icon.appendChild(doc.createElement('href')).appendChild(doc.createTextNode(nomimg))
        Icon.appendChild(doc.createElement('viewBoundScale')).appendChild(doc.createTextNode('0.75'))
        LatLonBox = GroundOverlay.appendChild(doc.createElement('LatLonBox'))
        LatLonBox.appendChild(doc.createElement('north')).appendChild(doc.createTextNode(str(north)))
        LatLonBox.appendChild(doc.createElement('south')).appendChild(doc.createTextNode(str(south)))
        LatLonBox.appendChild(doc.createElement('east')).appendChild(doc.createTextNode(str(east)))
        LatLonBox.appendChild(doc.createElement('west')).appendChild(doc.createTextNode(str(west)))
        kmz.writestr('doc.kml', doc.toxml())

        # libérer les ressources
        doc.unlink()
        kmz.close()
        
    

def TraiterKMZ(nomfich):
    "fait en sorte que les images présentes dans le KMZ ne dépassent pas la taille maximale"

    # ouvrir le fichier kmz avec zipfile
    kmzE = zipfile.ZipFile(nomfich+'.kmz','r')

    try:

        # vérifier qu'il y a bien un fichier appelé doc.kml et que c'est du XML
        try:
            docE = xml.dom.minidom.parse(kmzE.open('doc.kml','r'))
        except KeyError:
            print "le fichier %s ne contient pas un doc.kml qui soit du XML"%nomfichE

        # passer en revue toutes les images mentionnées dans doc.kml (boucle inutile, il n'y en a qu'un seul)
        for GroundOverlay in docE.getElementsByTagName('GroundOverlay'):
            # obtenir les différentes informations
            name = GroundOverlay.getElementsByTagName('name')[0].firstChild.data.strip()
            color = GroundOverlay.getElementsByTagName('color')[0].firstChild.data.strip()
            icon = GroundOverlay.getElementsByTagName('Icon')[0]
            href = icon.getElementsByTagName('href')[0].firstChild.data.strip()
            view = icon.getElementsByTagName('viewBoundScale')[0].firstChild.data.strip()
            LatLonBox = GroundOverlay.getElementsByTagName('LatLonBox')[0]
            north = float(LatLonBox.getElementsByTagName('north')[0].firstChild.data)
            south = float(LatLonBox.getElementsByTagName('south')[0].firstChild.data)
            east = float(LatLonBox.getElementsByTagName('east')[0].firstChild.data)
            west = float(LatLonBox.getElementsByTagName('west')[0].firstChild.data)
            print nomfich,':',name,',',href

            # charger l'image pour avoir sa taille et éventuellement la couper en deux
            fich = kmzE.open(href,'r')
            image = Image.open(StringIO.StringIO(fich.read()))
            fich.close()

            if href.startswith('files/'): href=href[6:]
            TraiterImage('', nomfich, image, name,href,north,south,east,west)

        docE.unlink()

    finally:
        # fermer le kmz d'entrée
        kmzE.close()

for nomfich in os.listdir('.'):
    if os.path.isfile(nomfich):
        nomfich,ext = os.path.splitext(nomfich)
        if ext == '.kmz':
            TraiterKMZ(nomfich)

Ce script ne fonctionne qu'avec python2 car il importe PIL (Image) qui n'a pas été portée à cette date sur python3.

J'ai juste un regret sur le format kmz (un fichier compressé par zip contenant un fichier xml appelé doc.kml et files/image.jpg), c'est que le doc.kml ne peut référencer qu'une seule image jpg. Il n'y a qu'un seul tag GroundOverlay possible dans ce fichier. Rien n'empêcherait d'en avoir plusieurs, mais Google Earth ne les accepte pas. Et les superoverlays ne sont pas gérés par les Garmin Oregon.