17
13 aoû 2009

Introduction


Né de la fusion d'OpenLayers et d'Ext, GeoExt est une librairie javascript permettant de créer facilement des d'interfaces cartographiques riches. Bien plus qu'un simple portage des deux librairies mères, GeoExt a complètement repensé les modèles de classes initiaux pour proposer au final de nouveaux objets complètement personnalisés. Néanmoins, celle-ci peut être difficile à appréhender du fait du mélange des genres entre OpenLayers et Ext. De ce fait, il est important de bien comprendre le rôle joué par chacune des composantes.

geo_ext_lib.png

Nous découvrirons au cours de ce tutoriel les éléments principaux de GeoExt, vous pouvez également vous référez aux tutoriaux ext-primer, openlayers-primer et geoext-quickstart réalisés par l'équipe de GeoExt.

Avant de construire notre première application, il convient d'étudier un peu l'architecture des classes de GeoExt. Celle-ci se divise en différentes catégories que sont :

  • Widgets : Composants liés à la carte (fonctionnalités, actions...) - Exemple
  • Form : Composants qui étendent la classe Ext.Form pour permettre la gestion des éléments cartographiques - Exemple
  • Grid : Composants qui étendent la classe classe Ext.grid pour permettre la gestion des éléments cartographiques - Exemple
  • tree : Composants qui étendent la classe classe Ext.tree pour permettre la gestion des éléments cartographiques - Exemple
  • data : Composants qui étendent la classe classe Ext.data pour permettre la gestion de données cartographiques - Exemple

L'objet MapPanel, coeur de l'application


L'objet Map d'OpenLayers et l'objet Panel d'Ext sont les objets centraux de ces librairies. La fusion de ces deux classes à donné naissance dans GeoExt à l'objet MapPanel. C'est lui qui aura à charge de "construire" la carte ainsi que son conteneur (panel).

L'objet ViewPort, que nous utilisons dans l'exemple ci-dessous, à la particularité de prendre automatiquement tout l'espace disponible. C'est à l'intérieur de celui-ci que nous allons construire les différents éléments de notre application. Pour cela nous spécifions que la propriété layout de l'objet Viewport est de type border. Cela signifie que son espace interne sera divisé en 5 zones (north, east, south, west et center). La zone "center" prenant automatiquement tout le reste d'espace disponible en fonction des dimensions des autres panels.

Étudions en détail l'exemple ci-dessous :

  • [1] - Liste des couches qui sera ajoutée ensuite à l'objet MapPanel
  • [2] - Construction de l'objet MapPanel.
  • [3] - Panel qui accueillera ensuite une légende, un tableau de données ...
  • [4] - Construction de l'objet Ext 'ViewPort" auquel on ajoute l'objet MapPanel


Ext.onReady(function() {
// [1] - layer
var bluemarble = new OpenLayers.Layer.WMS(
"bluemarble"
,"http://sigma.openplans.org/geoserver/wms?"
,{layers: 'bluemarble'}
);

// [2] - MapPanel
var mapUI = new GeoExt.MapPanel({
map: {
controls: [new OpenLayers.Control.Navigation()]
}
,region : 'center'
,title : 'map'
,layers: [bluemarble]
,extent: [-5, 35, 15, 55]
});

// [3] - Data Panel
var dataPanel = new Ext.Panel({
region : 'west'
,layout : 'fit'
,width : 150
});

// [4] - Final User Interface
new Ext.Viewport({
layout: "border"
,items: [
mapUI
,dataPanel
]
});
}); //EOF Ext.onReady

Il est déjà facile de voir l'un des avantages de GeoExt, le prototypage rapide d'interface. En effet, en quelques lignes de code nous avons dors et déjà une application fonctionnelle qui pourra servir ensuite de base à une réflexion plus approfondie.

Enrichir l'interface


Nous allons maintenant habiller notre interface pour cela GeoExt propose plusieurs classes regroupées au sein de la classe parent Widgets. Celle que nous allons utiliser est ZoomSlider qui permet de disposer d'un outil de zoom par niveaux. De plus nous allons en profiter pour améliorer un peu le design de notre application en modifiant notre fichier CSS.

Regardons en détail notre exemple d'enrichissement de l'interface :

  • [1] - Nous construisons notre objet ZoomSlider dont le but est d'offrir un outil de zoom avant et arrière.
  • [2] - Par rapport à l'exemple précédent l'attribut items a été ajouté à l'objet mapPanel. Il référence notre objet ZoomSlider.


// [1] - ZoomSlider
var zSlider = new GeoExt.ZoomSlider({
vertical: true
,height: 110
,x: 18
,y: 85
,map: mapPanel
,plugins: new GeoExt.ZoomSliderTip({
template: '

Zoom Level: {zoom}

'
})
});

// [2] - MapPanel
var mapPanel = new GeoExt.MapPanel({
map: {
controls: [
new OpenLayers.Control.Navigation()
,new OpenLayers.Control.PanPanel()
,new OpenLayers.Control.ZoomPanel()
]
}
,region : 'center'
,title : 'map'
,layers: [bluemarble]
,extent: [-5, 35, 15, 55]
,items : [zSlider]
});

Dans notre exemple nous avons fait le choix de n'afficher que le niveau de zoom lors de l'utilisation du zoomSlider. Mais vous pouvez également y ajouter l'échelle ou la résolution en cours.

Comme vous avez pu le constater l'ajout d'une nouvelle fonctionnalité ne nous a pas obligé à casser tout notre code. L'architecture est dès le départ bien pensée (Merci Ext).

Ajout de l'arbre de couches


Il existe différentes façons de créer notre arbre de couche. Soit en instanciant un à un à chacun des objets ou alors en créant un "pseudo" fichier de configuration. Nous verrons successivement chacune d'entre elles.

La création initiale de l'arbre de couches se fait via l'objet Ext TreeNode auquel nous ajoutons ensuite des "feuilles" via la méthode appendChild. GeoExt permet grâce aux méthodes GeoExt.tree.BaseLayerContainer et GeoExt.tree.OverlayLayerContainer de créer plus facilement les "feuilles" de cet arbre en se basant sur l'organisation des couches d'OpenLayers

Étudions en détails l'exemple de création de notre arbre de couches :

  • [1] - Création de la racine de l'arbre (la base).
  • [2] - Ajout des couches de base.
  • [3] - Ajout des couches Overlay.
  • [4] - Création du panel contenant notre arbre de couche.


//[1] - Tree Layer
var layerRoot = new Ext.tree.TreeNode({
text : "All Layers"
,expanded : true
,loader: new GeoExt.tree.LayerLoader({
applyLoader : true
})
});

//[2] - Base Layers
layerRoot.appendChild(new GeoExt.tree.BaseLayerContainer({
text : "Base Layers"
,map : mapPanel.map
,expanded : true
}));

//[3] - Overlay
layerRoot.appendChild(new GeoExt.tree.OverlayLayerContainer({
text : "Overlays"
,map : mapPanel.map
,expanded : true
}));

//[4] - Creation du panel
var layerTree = new Ext.tree.TreePanel({
title : "Map Layers"
,root : layerRoot
,expanded : true
,animate : true
});

Plutôt sympa comme résultat non? Et tout ça en à peine une vingtaine de ligne de code. Néanmoins, cela fait beaucoup d'objet à créer non? Ne serait-il pas plus facile d'utiliser un fichier de configuration? Comme cela est visible sur cet exemple, cela est tout à fait possible, testons-le immédiatement :


var treeConfig = new OpenLayers.Format.JSON().write([
{
nodeType : 'gx_baselayercontainer'
,expanded : true
,allowDrag : false
,allowDrop : false
,draggable : false
,icon : './img/map.png'
},{
text : 'Overlays'
,icon : './img/maps-stack.png'
,expanded : true
,children : [
{
nodeType : 'gx_layer'
,draggable : false
,layer : 'city'
,qtip : "Villes d'Europe"
,icon : './img/city-16x16.png'
},{
nodeType : 'gx_layer'
,layer : 'moutains'
,qtip : "Montagnes d'Europe"
,icon : './img/Mountain-16x16.png'
}
]
}
], true);

La variable treeConfig contient toute l'architecture de l'arbre de couches. L'exemple n'est pas difficile à comprendre mais j'aimerais attirer votre attention sur la propriété nodeType. Celle-ci peut prendre trois valeurs : gx_baselayercontainer, gx_overlaylayerontainer (classe LayerNode) et gx_layer (classe LayerNode) qui correspond successivement aux couches de bases, aux couches superposables et à toutes les couches. C'est cette dernière qui nous interesse car nous allons pouvoir construire notre branche en spécifiant exactement la couche correspondante tout en lui ajoutant des options de personnalisation (comme une icon ou un qtip).

Comme nous souhaitons également ajouter plus tard un tableau de données à ce panel (zone), nous allons immédiatement modifier ses caractéristiques afin qu'il se comporte comme un accordéon :


var accordion = new Ext.Panel({
margins : '5 0 5 5'
,split : true
,width : 160
,layout :'accordion'
,items : [layerTree]
});

//Data Panel
var dataPanel = new Ext.Panel({
title : 'Legend & Data'
,region : 'west'
,layout : 'fit'
,width : 160
,items : [accordion]
});

Passons maintenant à notre tableau de données.

Ajout des données vecteurs


Si vous n'avez pas encore été convaincu par GeoExt, l'objet FeatureStore de la classe data vous fera certainement basculer définitivement. Imaginez simplement que vous puissiez, à partir d'un simple objet, créer un tableau de données ainsi que les données cartographiques associées et qu'en plus ces deux éléments soient liés entre eux. C'est ce que permet FeatureStore auquel nous avons ajouté dans l'exemple ci-dessous l'objet FeatureSelectionModel dont le but est de gérer les interactions entre les deux éléments.

Étudions en détails notre exemple :

  • [1] - Creation de l'objet FeatureStore (entrepôt de données) dans lequel nous définissons la structure des données (fields).
  • [2] - Creation de l'objet Grid (tableau) contenant l'entrepôt de données. Nous spécifions également que la propriété sm (selection model) prend pour valeur FeatureSelectionModel
  • [3] - Ajout du tableau dans l'application.


// [1] - Creation de l'entrepot de donnees
store = new GeoExt.data.FeatureStore({
layer: city
,fields: [
{name: 'Name', type: 'string'}
,{name: 'Country', type: 'string'}
]
});

// [2] - Creation du tableau
gridPanel = new Ext.grid.GridPanel({
title: "Feature Grid"
,store: store
,columns: [{
header: "Name"
,width: 100
,dataIndex: "Name"
}, {
header: "Country"
,width: 60
,dataIndex: "Country"
}],
sm: new GeoExt.grid.FeatureSelectionModel()
});

// [3] - Ajout du tableau a l'interface
var grid = new Ext.Panel({
title : 'Grid'
,layout :'fit'
,items : [gridPanel]
});

Affichage d'une infobulle


La création d'une infobulle se fait via la classe GeoExt.Popup. Sa création n'a rien de compliqué et se passe je pense de commentaire :

Ci-dessous le code ayant servi à notre exemple :


city.events.on({
featureselected: function(e) {
if(typeof(popup) != "undefined"){
popup.destroy();
}
var content = ""+e.feature.attributes.Name+
"

Lon : "+e.feature.attributes.Longitude+
"
Lat : "+e.feature.attributes.Latitude;

popup = new GeoExt.Popup({
title : 'City'
,feature : e.feature
,width : 200
,html : content
,collapsible: true
,anchored : true
});
popup.show();
}
});

Conclusion


J'espère que ce petit tour d'horizon de GeoExt vous donnera envie de continuer dans l'exploration de cette fantastique librairie. Le mariage de deux librairies dont les objectifs et les concepts sont différents n'est pas une chose aisée et pourtant les développeurs de GeoExt ont réussi ce tour de force.

A propos de l'auteur: 
Arnaud Vandecasteele

Fervent défenseur de l'Open Source, Arnaud s'est spécialisé dans le développement d'application cartographiques web. OpenLayers, PostGIS ou encore Django sont autant d'outils qu'il manipule au quotidien.
S'il n'est pas en face de son ordinateur, vous le retrouverez un GPS à la main en train de cartographier pour OpenStreetMap, de faire voler son drone ou sur un tatami !

Commentaires

Problème installation GeoExt.

Merci pour cet excellent article.

Il manque juste un complément d'information sur l'installation de la librairie GeoExt. Lorsque j'ai voulu faire fonctionner en local votre exemple, j'ai du installer GeoExt à partir de http://svn.geoext.org/core/trunk et pas en partant du package GeoExt-release-0.5.zip disponible sur leur site. En effet dans votre dernière exemple on fait référence à un javascript "../../../lib/js/geoext/geoext/lib/GeoExt.js" (qui lui même fait appel à des scripts dans data/ ) qui ne sont pas dans le GeoExt-release-0.5.zip. Où alors quelque chose m'a échapper ?

Cela dit, cela marche très bien si je fait un export SVN de http://svn.geoext.org/core/trunk.

Bonne journée.

Bonjour,

J'utilise effectivement la version SVN de GeoExt. C'est beaucoup plus simple à gérer, un simple svn update est ma librairie est à jour :)

Arnaud

Tres interessant,

Cependant, le Pop Up ne s'affiche pas très clairement les bords gauche et droite sont transparents...quel peut être la raison. Le chargement n'est pas complet?

Sinon lors de la sélection de l'objet, comment permettre le déplacement vers l'objet?
Le pop-Up s'affiche mais l'objet n'est pas visible car hors-cadre.

Merci encore

Arno

Bonjour Arno,

Avec la propriété panIn à true (normalement par défaut) lors de la sélection la carte devrait automatiquement se centrer sur le popup. Concernant les bords transparents de mon côté je ne remarque rien de spécial.

Arnaud

Bonjour,

super tuto, le seul problème est que les liens vers les exemples sont rompus.

Merci

Pour moi les liens fonctionnent ! :)

Effectivement un bon tuto, par contre je n'arrive pas a trouver comment faire pour que le mapPanel fasse 100% de la hauteur et de la largeur!
New GeoExt.MapPanel({
title: "GeoExt MapPanel",
renderTo: "map",
height: '100%', width: '100%',
map: map,
});
Ca ne fait absolument rien, la page reste blanche.

Avec OpenLayer tout seul, je mets dans le css de ma div :
#map
{height:100%, width:100%}
et cela fonctionne très bien! Ici non je suis obligé de mettre une taille fixe!
Des idées?

Bonjour,

Pour cela, il faut inclure votre MapPanel dans un objet de type ViewPort. Celui-ci posède la particularité de prendre toute la place disponible. Ensuite votre mappanel doit etre placé dans la région "center".

Cela devrait alors fonctionner.

Arnaud

Merci ca fonctionne très bien!

Grâce à cet excellent tuto j'ai réussi à à donner vie à mon projet. Il me reste juste un petit problème car je n'arrive pas à faire un Affichage d'une infobulle sur plusieur couches. Sur une couche il n'y à aucun problème

Merci pour ce travail.

Comment faites vous lorsque vous rajoutez le zoomslider pour masquer (voir même supprimer) le contrôle de déplacement et zoom d'openlayers ?
Chez moi les 2 se superposent ...
Merci

Ludo

Désolé pour le dérangement j'ai trouvé une méthode en faisant
var panZoom = mapPanel.map.getControlsByClass('OpenLayers.Control.PanZoom');
panZoom[0].destroy();

Bonjour,

Avez-vous le code qui appelle les controls ?
Il semblerait que vous instanciez plusieurs fois les controls de zoom.

Arnaud

Salut Arnaud
Je n'appelle à priori jamais ce contrôle
Les seuls contrôles appelé sont :
MousePosition, ScaleBar, ZoomBox et Measure
J'ai l'impression que le panZoom est intégré par défaut

Ludo

Bonjour,

Je voudrais pas abuser de votre temps mais je cherche à intégrer non-pas bluemarble mais le référentiel Google Maps.
Et là je suis très gếné car le remplacement de

// [1] - layer
var bluemarble = new OpenLayers.Layer.WMS(
"bluemarble"
,"http://sigma.openplans.org/geoserver/wms?"
,{layers: 'bluemarble'}
);

par une couche gmap provoque des problèmes.
La projection est modifiée car 900913, ce qui provoque une reprojection de mes KML.

Ensuite les Bounds et SetCenter...la je ne sais plus trop quoi modifié pour que ce soit fonctionnel.

Avez vous de la doc ou conseils qui me permettrait de m'en sortir.
Géomatiquement
Arno

Salut Arno,
je t'ai répondu sur ce post du ForumSIG.
Manifestement sans succès.

As-tu essayé de commencer avec une simple carte GeoExt, sans Viewport, sans Tree, etc, et de modifier la pojection pour afficher le fond Google Maps ?
Puis ensuite d'ajouter un fichier KML ?

Dans le lien donné, tu n'a pas modifié le setCenter (ie. map.setCenter(new OpenLayers.LonLat(23.49,39)); fin du fichier init7.js ) ??? comme conseillé.

Tu as une clé dans l'appel Javascript à Google Maps : bizarre, il n'y a en pas besoin pour la version 3 normalement. Je te conseille aussi de mettre v=3.2 pour cet appel à l'API, ça évitera l'affichage d'une popup disgracieuse.

Tu peux t'inspirer du code de cette application Hillshading pour voir comment j'ai initialisé la projection 900913 (ie. Google en Leet Speak).

On continue dans le forum qui a plus de lisibilité.

Fabien (aka GobbaF)

bonjour,

J'ai deux questions concernant le tuto que je ne cesse de consulter:

1- Je souhaite réaliser un LayerTree.

Seulement je n'utilise ni JSON ni WMS.
J'utilise des layers en GML.
Comment puis-je le configuer sachant que le treeconfig me semble le plus simple dans mon cas, et que je n'ai pas de mapPanel de définit(ViewPort)?

2- Quel code assure l'ouverture de popup sur carte lors du clic dans le FeatureStore?

Merci d'avance de votre retour.
Arno