function Carte(parametres, articles, links) {

	var object = this;
	
	this.parametres = {
		// Module dimensions and caracteristics
		width: 1440,
		height: 800,
		top: 50,
		left: -240,
		name: null,
		boxID: "box",
		selectorID: "carto",
		canvasID: "canvas",
		info: "infos",
		
		couleurs: null,
		relations: null,
		
		// How to deal with the data
		isPositions: false,
		
		charge: -30,
		gravity: .13,
		theta: 3,
		
		// Node rendering
		nodeSizeInterval: [3, 15],
		log: false,
		exp: false,
		layerStyle: {
			opacity: 0.6
		},
		handle_len_rate: 2.2,
		maxDistance: 35,
		
		scaleInterval: [0.5, 10],
		
		infobulleHtml: function(node) {
			return node.titre + "<p>cluster: " + node.group +"</p>";
		},
		
		activerZoom: true,
		activerNodeZoom: true,
		activerInfobulle: true,
		zoomNode: false,
		logZoomNode: false,
		logZoomNodeParameter: 2
	};
	
	this.beginDate = new Date(); 
	this.etendre(parametres);
	
	if (null==this.parametres.name) {
		this.parametres.name = this.parametres.selectorID;
	}
	
	//création du contenant sur la page, si inexistant
	if (null == (document.getElementById(this.parametres.boxID))) {
		d3.select('.content').append('div')
			.attr("id", this.parametres.boxID)
			.classed("gallery", true)
			.style("position", "absolute")
			.style("border", "1 px dashed");
	}
	
	d3.select('#' + this.parametres.boxID)
		.style("width", this.parametres.width)
		.style("height", this.parametres.height)
		.style("position", "absolute")
		.style("top", this.parametres.top)
		.style("left", this.parametres.left)
		.style("overflow", "hidden")
		.style('z-index', 0);
	
	
	if (null == (document.getElementById(this.parametres.selectorID))) {
		d3.select('.gallery').append('div')
			.attr("id", this.parametres.selectorID);
	}
	
	d3.select('#' + this.parametres.selectorID)
		.attr("width", this.parametres.width)
		.attr("height", this.parametres.height)
		.style("position", "absolute")
		.style('z-index', 2);
	
	// Create drag interaction with the module
	this.drag = d3.behavior.drag()
	    .on("dragstart", function(d, i) {object.dragstart(d, i, object);})
	    .on("drag", object.dragmove)
	    .on("dragend", function(d, i) {object.dragend(d, i, object);});
		   
	this.static = false;
    
    if (this.parametres.activerNodeZoom) {
    		this.nodeMouseover = function(node) { 
    			var scale = object.getScale(object.rect);
				object.grow(node, scale); 
			};
			
			this.nodeMouseout = function(node) {
				var radius = object.radius(node.datum("scaledPoids"));
				object.shrink(radius, node); 
			};
    }
    
    if (this.parametres.activerInfobulle) {
    	
		this.nodeClick = function(d) { 
			object.highlight(d); 
		};
		
		this.infoClick = function(d) { 
			object.blur(); 
		};
    }	
	
	//création de l'infobulle, si inexistante
	if (null == (this.infoBulle = d3.select('#' + this.parametres.info)[0][0])) {
		this.infoBulle = d3.select('#' + this.parametres.selectorID).append('div')
			.attr("id", this.parametres.info)
			.style("top", this.parametres.height)
			.style("opacity", 0)
			.style("z-index", 1);
	}
	
	if (null!=this.parametres.relations) {
		this.parametres.relations.add(
				this.parametres.name + ".infobulle", this.infoBulle)
			.click(this.infoClick);
	}
	else {
		this.infoBulle.on("click", this.infoClick);
	}
	
	
	//création du canvas sur la page, si inexistant
	if (null == (this.canvas = document.getElementById(this.parametres.canvasID))) {
		d3.select('.gallery').insert('canvas', '#' + this.parametres.selectorID)
			.attr("id", this.parametres.canvasID)
			.attr("resize", false);
		this.canvas = document.getElementById(this.parametres.canvasID);
	}
	d3.select('#' + this.parametres.canvasID)
		.attr("width", d3.select('#' + this.parametres.selectorID).attr("width"))
		.attr("height", d3.select('#' + this.parametres.selectorID).attr("height"))
		.style("position", "absolute")
		.style("z-index", 1);
	
	this.mouse = {};
	this.isDown = false;
	this.drag2 = false;
	
	this.svg = d3.select('#' + this.parametres.selectorID).append('svg')
		.attr("width", this.parametres.width)
		.attr("height", this.parametres.height)
		.on("mousedown", function() {object.mouseDown(event);})
		.on("mousemove", function() {object.mouseMove(event);})
		.on("mouseup", function() {object.mouseUp(event);});
		
	paper.setup(this.canvas);
	paper.view.viewSize = new paper.Size(object.parametres.width, object.parametres.height);
	
	this.clusters = articles.clusters;
		
	//génération d'une palette de couleurs
	if (null == this.parametres.couleurs) {
		//this.parametres.couleurs = this.selectColors(this.clusters.length);
		this.parametres.couleurs = new Couleurs(this.clusters.length);
	}
	
	console.log("Environnement OK.");
		
	this.data = {};
	this.zones = [];	
	
	if (this.parametres.isPositions) {
		// Use directly data
		this.data.nodes = this.generateNodes(articles, this.parametres.isPositions);
		this.length = articles.nbItems;
		this.display();
		
	}
	else {
		//initialisation du Force Directed Graph
		this.data.nodes = this.generateNodes(articles, this.parametres.isPositions);
		this.data.links = this.generateLinks(links, this.data.nodes);
	
		this.force = d3.layout.force()
			.charge(this.parametres.charge)    //-20  
			.nodes(this.data.nodes)
			.links(this.data.links)
			.linkDistance(function(link) {return link.linkDistance;}) //20
			.linkStrength(function(link) {return link.linkStrength;})
			.size([this.parametres.width, this.parametres.height])
			.gravity(this.parametres.gravity) //.1
			.theta(this.parametres.theta)
			.start();
			
		console.log("Graphe en cours de stabilisation . . .");
		
		this.force.on("tick", function() {
			console.log("Stabilisation OK.");
			object.display();
		});
	
	}	
};	


		
Carte.prototype = {
	constructor: Carte,
		
	//gestion des paramètres
	etendre: function(parametres) {
		if (null==parametres || "object"!=typeof(parametres)) {
			return;
		}
		
		for (var cle in parametres) {
			this.parametres[cle] = parametres[cle];
		}
	},
	
	display: function () {
		
		this.showNodes();
		if (this.parametres.activerZoom) {
			this.activateZoom();
		}
		
		paper.setup(this.canvas);
		paper.view.viewSize = new paper.Size(
				this.parametres.width, this.parametres.height);
		this.createBackground();
	},
	
	activateZoom: function() {
		this.rect = this.svg.selectAll("rect")
		    .data([{
		    	x: this.parametres.width*(1 - this.parametres.scaleInterval[0])/(this.parametres.scaleInterval[1] - this.parametres.scaleInterval[0]),
		    	y: 0
		    }])
			.enter().append("rect")
			.attr("x", function(d) { return d.x; })
			.attr("y", function(d) { return d.y; })
		    .attr("width", 15)
		    .attr("height", 15)
		    .style("stroke", "black")
		    .style("fill", "#00f")
		    .style("z-index", 2)
			.call(this.drag);
		
		if (null!=this.parametres.relations) {
			this.parametres.relations.add(
					this.parametres.name + ".zoomButton", this.rect);
		}
	},
	
	//génération d'une palette de couleurs
	selectColors: function(nbClusters) {
		var colors = colorbrewer;
		var sets = ["BrBG", "PiYG", "PRGn", "PuOr", "RdBu", "RdYlBu", "RdYlGn"];
		var selectorID, finalColors = [];
		
		while (finalColors.length < nbClusters) {
			selectorID = 	Math.floor(Math.random()*7);
			selectorID2 = Math.floor(Math.random()*11);
			color = new paper.RgbColor(colors[sets[selectorID]][11][selectorID2]);
			
			var available = true;
			for (var i = 0; i < finalColors.length; i++) {
				if (Math.floor(finalColors[i].hue) == Math.floor(color.hue)) {
					available = false;
				}
			}
			
			if (color.brightness < 0.9 && available) {
				finalColors.push(color.toCssString());
			}
		}
		
		return finalColors;
	},
	
	//génération des noeuds √† partir du json
	generateNodes: function(items, isPos) {
		
		var nodes = [];
		var poidsMin = 100000, poidsMax = 0;
		
		// on crée les noeuds
		for (var i = 0 ; i<items.nbItems; ++i) {
			nodes.push({index: i, titre: "Titre numero " + i, weigth: 1});
		}
		
		// on donne √† chaque noeud son poids et couleurs
		// on traite identiquement core et agregated
		// pour les clusters
		var nbClusters = items.clusters.length;
		for (var i =0; i< nbClusters; ++i) {
			var cluster = items.clusters[i], 
				clusterId = cluster.id,
				groups = [];
			if (cluster.items) {
				groups.push("items");
			}
			else {
				groups.push("core");
				groups.push("agregated");
			}
			for (var a in groups) {
				var group = cluster[groups[a]];
				for (var j =0; j<group.length; ++j) {
					var item = group[j], poids = parseInt(item.value);
					nodes[item.key]["poids"] = poids;
					nodes[item.key]["scaledPoids"] = poids;
					nodes[item.key]["group"] = clusterId;
					nodes[item.key]["key"] = item.key;
					if (isPos) {
						nodes[item.key]["x"] = parseFloat(item.x);
						nodes[item.key]["y"] = parseFloat(item.y);
					}
					else {
						nodes[item.key]["x"] = this.parametres.width/2*(1 + Math.cos(i*Math.Pi/nbClusters));
						nodes[item.key]["y"] = this.parametres.height/2*(1 + Math.sin(i*Math.Pi/nbClusters));
					}
					if (null != this.parametres.relations) {
						this.parametres.relations.add(this.parametres.selectorID + ".node." + item.key, nodes[item.key]);
					}
					if (poids>poidsMax) {
						poidsMax = poids;
					}
					else if (poids<poidsMin) {
						poidsMin = poids;
					}
				}
			}
		}
		
		this.poidsMin = poidsMin;
		this.poidsMax = poidsMax;
		
		// pour les unclustered
		var unclusteredId = items.clusters.length;
		var unclusteredItems = (items.unclustered.items)? 
				items.unclustered.items: items.unclustered;
		for (var i =0; i< unclusteredItems.length; ++i) {
			var item = unclusteredItems[i];
			nodes[item.key]["poids"] = poidsMin;
			nodes[item.key]["scaledPoids"] = poidsMin;
			nodes[item.key]["group"] = unclusteredId;
			nodes[item.key]["key"] = item.key;
			if (isPos) {
				nodes[item.key]["x"] = parseFloat(item.x);
				nodes[item.key]["y"] = parseFloat(item.y);
			}
			else {
				nodes[item.key]["x"] = this.parametres.width/2;
				nodes[item.key]["y"] = this.parametres.height/2;
			}
		}
		
		console.log("Data articles extraites.");
		return nodes;
	},
	
	//génération des liens à partir du json
	generateLinks: function(liens, nodes) {
		//var linkMax = 0, linkMin = 500;
		var links = [];
		
		for (var i = 0; i< liens.images.length; ++i) {
			var item = liens.images[i];
			for (var j = 0; j < 20 && j < item.v.length; ++j) {
				var voisin = item.v[j];
				
				var linkDistance = 200/Math.max(voisin.d, 4), linkStrength;
				
				if (nodes[item.id]["group"] == nodes[voisin.id]["group"]) {
					linkStrength = 1;
					linkDistance *= 0.5;
				}
				else {
					linkStrength = 0.5;
				}
				
				links.push({source: item.id, target: voisin.id, value: voisin.d, linkDistance: linkDistance, linkStrength: linkStrength});
			}
		}
		
		console.log("Data liens extraites.");
		return links;
	},

	//calcul du rayon
	radius: function(poids) {
		
		var diff = this.parametres.nodeSizeInterval[1] - this.parametres.nodeSizeInterval[0];
		scale = this.getScale(this.rect);
		
		if (this.parametres.log) {
			return this.parametres.nodeSizeInterval[0] + Math.log(poids/(this.poidsMin*scale))*diff/Math.log(this.poidsMax/this.poidsMin);
			
		}
		else if (this.parametres.exp) {
			return this.parametres.nodeSizeInterval[0]*Math.exp(Math.log(this.parametres.nodeSizeInterval[1]/this.parametres.nodeSizeInterval[0])*(poids-(this.poidsMin*scale))/(scale*(this.poidsMax - this.poidsMin)));
		}
		else {
			return scale*((this.parametres.nodeSizeInterval[0] + diff*(poids - scale*this.poidsMin)/(scale*(this.poidsMax - this.poidsMin))));
		}
	},
	
	//affichages des noeuds
	showNodes: function() {
		var object = this;
		node = this.svg.selectAll("circle.node")
			 .data(this.data.nodes)
			 .enter().append("circle")
			 .attr("class", "node")
			 .attr("cx", function(d) { return d.x; })
			 .attr("cy", function(d) { return d.y; })
			 .attr("z-index", 0)
			 .attr("r", function(d) {return object.radius(d.poids);})
			 .style("fill", function(d) { 
				 return object.parametres.couleurs.get(d.group);
			 });
			 /*.on("mouseover", this.nodeMouseover)
			 .on("mouseout", this.nodeMouseout)*/;
		
		// Register all nodes
		if (null!=object.parametres.relations) {
			 node.each(function(datum, index) {
				 datum["registrationName"] = object.parametres.name + ".node." + index; 
				 object.parametres.relations.add(
						 datum.registrationName, d3.select(this))
					.mouseover(object.nodeMouseover)
					.mouseout(object.nodeMouseout);
		 	});
		 }
	
		node.append("p")
			.text(function(d) { return d.titre; });
		node.on('click', this.nodeClick);
		console.log("Noeuds OK.");
	},
	
	//génération du fond de la carte
	createBackground: function() {
		var circlePaths = [];
		scale = this.getScale(this.rect);
		object = this;
		
		this.svg.selectAll('circle.node').each(function(d) {
			var node = d3.select(this);
			var circle = new paper.Path.Circle(
					new paper.Point(d.x, d.y), object.radius(d.scaledPoids)*2);
			circle.fillColor = node.style("fill");
			circlePaths.push(circle);
		});
		
		console.log("Fond des noeuds OK.");
		this.generateConnections(circlePaths);
		paper.view.draw();
		
		/*var endDate = new Date();
		var timer = endDate.getTime() - beginDate.getTime();
		console.log(timer);*/
	},
	
	//generate background connections between nodes of a same cluster
	generateConnections: function(paths) {
		for (var c=0; c<this.clusters.length; ++c) {
			var cluster = this.clusters[c];
			
			// définir les données suivant le type passé
			var group = cluster.items;
			if (undefined==group) {
				group = cluster.core.concat(cluster.agregated);
			}
				
			var layer = new paper.Layer();
			
			// Donner un design au fond
			for (var attr in this.parametres.layerStyle) {
				if (layer[attr]) {
					layer[attr] = this.parametres.layerStyle[attr];
				}
				else {
					layer.style[attr] = this.parametres.layerStyle[attr];
				}
			}
			
			var maxDistance = this.scale(this.rect[0][0].getAttribute("x"))*this.parametres.maxDistance,
				handle = this.parametres.handle_len_rate;
			for (var i = 1; i<group.length; ++i) {
				var circle = paths[group[i].key];
				if ((circle.position.x + maxDistance > 0) && (circle.position.x < this.parametres.width + maxDistance) && (circle.position.y + maxDistance > 0) && (circle.position.y < this.parametres.height + maxDistance)) {
					layer.appendTop(circle);
					for (var j = 0; j<i; ++j) {
						var path = this.connect(circle, paths[group[j].key], 0.5, handle, maxDistance);
						if (null!=path) {
							layer.appendTop(path);
						}
					}
				}
			}
			
			this.zones[c] = layer;
			if (null != this.parametres.relations) {
				this.parametres.relations.add(this.parametres.selectorID + ".layer." + c, layer);
			}
			
		}
		paper.project.layers[0].remove();
		console.log("Connexions OK.");
	},
	
	/*
	 * Create a connection between two balls
	 */
	connect: function(ball1, ball2, v, handle_len_rate, maxDistance) {
	    var center1 = ball1.position;
	    var center2 = ball2.position;
	    var radius1 = ball1.bounds.width / 2;
	    var radius2 = ball2.bounds.width / 2;
	    var pi2 = Math.PI / 2;
	    var d = center1.getDistance(center2);
	    var u1, u2;
	
	    if (radius1 == 0 || radius2 == 0) {
	        return null;
		}
	
	    if (d > maxDistance || d <= Math.abs(radius1 - radius2)) {
	        return;
	    } else if (d < radius1 + radius2) { // case circles are overlapping
	        return null;
	    } else {
	        u1 = 0;
	        u2 = 0;
	    }
	    
	    var center3 = center2.clone();
	    center3.x = center2.x - center1.x;
	    center3.y = center2.y - center1.y;
	
	    var angle1 = center3.getAngleInRadians();
	    var angle2 = Math.acos((radius1 - radius2) / d);
	    var angle1a = angle1 + u1 + (angle2 - u1) * v;
	    var angle1b = angle1 - u1 - (angle2 - u1) * v;
	    var angle2a = angle1 + Math.PI - u2 - (Math.PI - u2 - angle2) * v;
	    var angle2b = angle1 - Math.PI + u2 + (Math.PI - u2 - angle2) * v;
	    var p1a = center1.add(this.getVector(angle1a, radius1));
	    var p1b = center1.add(this.getVector(angle1b, radius1));
	    var p2a = center2.add(this.getVector(angle2a, radius2));
	    var p2b = center2.add(this.getVector(angle2b, radius2));
	
	    // define handle length by the distance between
	    // both ends of the curve to draw
	    var totalRadius = (radius1 + radius2);
	    var p3a = p2a.clone();
	    p3a.x = p1a.x - p2a.x;
	    p3a.y = p1a.y - p2a.y;
	    var d2 = Math.min(v * handle_len_rate, p3a.length / totalRadius);
	
	    // case circles are overlapping:
	    d2 *= Math.min(1, d * 2 / (radius1 + radius2));
	
	    radius1 *= d2;
	    radius2 *= d2;
	
	    var path = new paper.Path([p1a, p2a, p2b, p1b]);
	    path.style = ball1.style;
	    path.closed = true;
	    var segments = path.segments;
	    segments[0].handleOut = this.getVector(angle1a - pi2, radius1);
	    segments[1].handleIn = this.getVector(angle2a + pi2, radius2);
	    segments[2].handleOut = this.getVector(angle2b - pi2, radius2);
	    segments[3].handleIn = this.getVector(angle1b + pi2, radius1);
	    return path;
	},
	
	// ------------------------------------------------
	getVector: function(radians, length) {
		var angle = radians * 180 / Math.PI;
	    return new paper.Point({"angle": angle,
	        "length": length
	    });
	},
	
	//apparition de l'infoBulle
	highlight: function(node) {
		scale = this.parametres.activerZoom ? this.scale(this.rect[0][0].getAttribute("x")) : 1;
		this.infoBulle.html(this.parametres.infobulleHtml(node));
		if (this.infoBulle.classed("selected")) {
			this.infoBulle.transition()
				.duration(500)
				.style("left", node.x + "px")
				.style("top", node.y + "px");
		}
		else {
			this.infoBulle.classed("selected", true)
				.style("left", node.x + "px")
				.style("top", node.y + "px")
				.transition()
				.duration(300)
				.style("opacity", 0.5);
		}
	},
	
	//disparition de l'infoBulle
	blur: function() {
		this.infoBulle.classed("selected", false)
			.transition()
			.duration(400)
			.style("opacity", 0);
		this.infoBulle.style("top", this.parametres.height);
	},
	
	//zoom sur un point
	grow: function(node, scale) {
		if (!this.static) {
			node.attr("z-index", 1)
				.transition()
				.duration(200)
				.attr("r", this.parametres.nodeSizeInterval[1]*scale);
		}
	},
	
	//dézoom sur un point
	shrink: function(radius, node) {
		if (!this.static) {
			node.transition()
				.duration(70)
				.attr("z-index", 0)
				.attr("r", radius);
		}
	
	},
	
	//réaffichage du fond sur recadrage du navigateur
	onResize: function(event) {
		paper.view.draw();
		d3.select('#' + this.parametres.canvasID)
			.attr("width", d3.select('#' + this.parametres.selectorID).attr("width"))
			.attr("height", d3.select('#' + this.parametres.selectorID).attr("height"));
	},
	
	dragstart: function(d,i, object) {
    	object.rect.xi = object.scale(d3.event.x);
    	object.static = true;
    },
    
	dragmove: function(d, i) {
		d.x += d3.event.dx;
		d3.select(this).attr("x", d.x);
	},
	
	dragend: function(d, i, object) {
		
		var ratio = object.scale(d3.event.x)/object.rect.xi;
		object.infoBulle.classed("selected", false)
			.style("opacity", 0);
			
		object.zoom(ratio);
		object.resetCanvas();
		
		object.zoomTransition(ratio);
		
		object.static = false;
		var timer = new Date().getTime() - object.beginDate.getTime();
		console.log(timer);
	},
	
	resetCanvas: function() {
		paper.view.remove();
		this.hideView();
		paper.setup(this.canvas);
		paper.view.viewSize = new paper.Size(this.parametres.width, this.parametres.height);
	},
	
	hideView: function() {
		var ctx = this.canvas.getContext('2d');
		ctx.fillStyle = 'rgb(0,0,0)';
		ctx.fillRect(this.canvas.left, this.canvas.top, this.canvas.width, this.canvas.height);
	},
	
	scale: function(x) {
		return (this.parametres.scaleInterval[0] + (this.parametres.scaleInterval[1] - this.parametres.scaleInterval[0])*(x/this.parametres.width));
	},
	
	getScale: function(rect) {
		if (rect == undefined || !this.parametres.activerZoom) {
			return 1;
		}
		else {
			if (this.parametres.zoomNode) {
				return this.scale(rect[0][0].getAttribute("x"));
			}
			else if (this.parametres.logZoomNode) {
				return Math.log(1 + this.scale(rect[0][0].getAttribute("x"))*this.parametres.logZoomNodeParameter)/(Math.log(1 + this.parametres.logZoomNodeParameter));
			}
			else {
				return 1;
			}
		}
	},
	
	zoom: function(ratio) {
		var object = this;
		var scale = this.getScale(this.rect);
		
		this.svg.selectAll("circle.node")
			.each(function(d) {
				d.x = object.parametres.width/2 
					- ratio*(object.parametres.width/2- d.x);
				
				d.y = object.parametres.height/2 
					- ratio*(object.parametres.height/2 - d.y);
				
				d.scaledPoids = d.poids*scale;
			});
	},
	
	zoomTransition: function(ratio) {
		var object = this;
		var c = 0;
		this.svg.selectAll("circle.node")
			.transition()
				.duration(Math.max(400*ratio, 400/ratio))
				.attr("cx", function(d) {
					return d.x;
				})
				.attr("cy", function(d) {
					return d.y;
				})
				.attr("r", function(d) { return object.radius(d.scaledPoids);})
				.each("end", function() {
					c++;
					if (c == object.length) {
						object.static = false;
						object.createBackground();
					}
				});
	},
	
	mouseDown: function(e) {
		this.isDown = true;
		this.mouse.x = e.x;
		this.mouse.y = e.y;
		this.center = {};
		/*this.center.x = paper.view.center.x;
		this.center.y = paper.view.center.y;*/
		var object = this;
		setTimeout(function() {
				if (object.isDown) {
					object.blur();
					//d3.select(object.canvas).style("visibility", "hidden");
					object.resetCanvas();
					object.static = true;
					object.drag2 = true;
				}	
			}, 100);
	},
	
	mouseMove: function(e) {
		if (this.drag2) {
			diff = {};
			diff.x = e.x - this.mouse.x;
			diff.y = e.y - this.mouse.y;
			/*this.center.x -= diff.x;
			this.center.y -= diff.y;*/
			this.svg.selectAll('circle.node')
				.attr("cx", function(d) {
					d.x += diff.x;
					return d.x;
					})
				.attr("cy", function(d) {
					d.y += diff.y;
					return d.y;
					});
			this.mouse.x = e.x; this.mouse.y = e.y;
		}
	},
	
	mouseUp: function(e) {
		if (this.drag2) {
			//paper.view.center = new paper.Point(this.center.x, this.center.y);
			//d3.select(object.canvas).style("visibility", "visible");
			this.createBackground();
			this.static = false;
			this.drag2 = false;
		}
		this.isDown = false;
		this.mouse = {};
	}
	
};