Show:

File: ia\maps\layers\FeatureLayer.js

/** 
 * The base class for feature layers (points, lines and polys).
 *
 * @author J Clare
 * @class ia.FeatureLayer
 * @extends ia.ItemLayer
 * @constructor
 * @param {String} inSource The spatial source.
 */
ia.FeatureLayer = function(inSource)
{
	ia.FeatureLayer.baseConstructor.call(this);

	this.dataLabel = "";
	this.minLabelExtent = -Infinity;
	this.maxLabelExtent = Infinity;
	this.symbolSize = 15;
	this.iconPath = "";
	this.eval = true;
	this.uid = "";
	this.symbol = ia.Shape.CIRCLE;
	this.selectionOpacity = 0.3;
	this.highlightOpacity = 0.3;

	if (inSource != null) this.source = inSource;
};
ia.extend(ia.ItemLayer, ia.FeatureLayer);

/**
 * The layer data label.
 *
 * @property dataLabel
 * @type String
 * @default ""
 */
ia.FeatureLayer.prototype.dataLabel;

/**
 * The minimum display extent for labels.
 *
 * @property url
 * @type Number
 * @default Infinity
 */
ia.FeatureLayer.prototype.minLabelExtent;

/**
 * The maximum display extent for labels.
 *
 * @property maxLabelExtent
 * @type Number
 * @default Infinity
 */
ia.FeatureLayer.prototype.maxLabelExtent;

/**
 * The symbol size.
 *
 * @property symbolSize
 * @type Number
 * @default 15
 */
ia.FeatureLayer.prototype.symbolSize;

/**
 * The icon iconPath.
 *
 * @property url
 * @type String
 * @default ""
 */
ia.FeatureLayer.prototype.iconPath;

/**
 * The loaded icon derived from iconPath.
 *
 * @property icon
 * @type Image
 */
ia.FeatureLayer.prototype.icon;
	
/** 
 * Check if its eval data.
 *
 * @property eval
 * @type Boolean
 * @default true
 */
ia.FeatureLayer.prototype.eval;

/** 
 * Unique id.
 *
 * @property uid
 * @type String
 * @default ""
 */
ia.FeatureLayer.prototype.uid;

/** 
 * The spatial data source for the layer.
 *
 * @property source
 * @type String
 */
ia.FeatureLayer.prototype.source;

/** 
 * The geometry for the layer.
 *
 * @property geometry
 * @type String
 */
ia.FeatureLayer.prototype.geometry;

/**
 * The symbol used for point data.
 *
 * @property symbol
 * @type String
 * @default ia.Shape.CIRCLE
 */
ia.FeatureLayer.prototype.symbol;

/** 
 * The selection opacity.
 *
 * @property selectionOpacity
 * @type Number
 * @default 0.3
 */
ia.FeatureLayer.prototype.selectionOpacity;

/** 
 * The highlight opacity.
 *
 * @property highlightOpacity
 * @type Number
 * @default 0.3
 */
ia.FeatureLayer.prototype.highlightOpacity;

/** 
 * Loads the source data.
 *
 * @method loadSource
 */	
ia.FeatureLayer.prototype.loadSource = function() 
{
	var me = this;

	ia.File.load(
	{
		url: me.source,
		dataType: "json", 
		onSuccess:function(json)
		{
			me.parseData(json);
			me.isLoaded = true;
			me.render();
			
			var e = new ia.Event(ia.Event.LAYER_READY, me);
			me.dispatchEvent(e);
		}
	});
};

/** 
 * Parses the data after its completed loading.
 *
 * @method parseData
 * @param {JSON} data The raw data. 
 */
ia.FeatureLayer.prototype.parseData = function(data)
{
	// This is all written for optimization - touch at your peril.
	this.items = {};
	this.itemArray = [];

	if (data.e !== undefined) this.eval = false;
	if (data.uid !== undefined) this.uid = data.uid;

	// This is the bBox that is used to adjust the layer coords and not the true bBox of the layer.
	var bb = data.boundingBox.split(" ");
	var bBox = new ia.BoundingBox(parseFloat(bb[0]),parseFloat(bb[1]),parseFloat(bb[2]),parseFloat(bb[3]));  

	var bx = bBox.getXMin();
	var by = bBox.getYMin();
	var bw = bBox.getWidth();
	var bh = bBox.getHeight();
	
	// Used to transform coordinates.
	var pixelWidth = parseFloat(data.pixelWidth);  
	var pixelHeight = parseFloat(data.pixelHeight);  
	
	var layerMinX = Infinity, layerMinY = Infinity, layerMaxX = -Infinity, layerMaxY = -Infinity;
	
	// JSON changes.
	// id > d
	// name > n
	// coords > p

	// Iterate through map features.
	var fLength = data.features.length;
	
	for (var i = 0; i < fLength; i++) 		
	{
		var feature = data.features[i];
	
		var item = {};
		item.state = ia.ItemLayer.UNSELECTED;
		item.layer = this;
		item.id = feature.d;
		item.name = feature.n;
		item.parent = this;
		item.symbolSize = this.symbolSize;
		if (this.geometry === "line") item.symbolSize =  this.style.lineWidth;
		
		if (this.geometry === "polygon" || this.geometry === "line") 
		{
			var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
		
			// Iterate through all polygons in this feature.
			var pLength = feature.p.length;
			var shapes = new Array();		

			for (var j = 0; j < pLength; j++) 	
			{
				var coords = feature.p[j];
				var cLength = coords.length - 1;	

				var shape = new Array();			
				var cx = 0;
				var cy = 0;

				// Iterate through all points in this polygon.
				for (var k = 0; k < cLength; k+=2) 
				{
					var p = new Object()
					cx = cx + coords[k];
					cy = cy + coords[k+1];
					p.x = bx + ((cx / pixelWidth) * bw);
					p.y = by + ((cy / pixelHeight) * bh);
					shape.push(p);

					// Calculation to find bBox of item.
					minX = (p.x < minX) ? p.x : minX;
					minY = (p.y < minY) ? p.y : minY;
					maxX = (p.x > maxX) ? p.x : maxX;                       
					maxY = (p.y > maxY) ? p.y : maxY;
				}
				shapes.push(shape);
			}
			item.shapes = shapes;
		}
		else 
		{
			// Convert each point back to real coords.
			var p = new Object();
			p.x = bx + ((feature.p[0] / pixelWidth) * bw);
			p.y = by + ((feature.p[1] / pixelHeight) * bh);
		
			// Calculation to find bBox of item.
			minX = p.x;
			minY = p.y;
			maxX = p.x;                       
			maxY = p.y;
			
			item.shapes = [p];
		}
		
		// Set the calculated item bBox.
		item.bBox = new ia.BoundingBox(minX, minY, maxX, maxY);
		item.size = Math.max(item.bBox.getWidth(), item.bBox.getHeight());
		this.items[item.id] = item;
		this.itemArray.push(item);
		
		// Calculation to find bBox of layer.
		layerMinX = (minX < layerMinX) ? minX : layerMinX;
		layerMinY = (minY < layerMinY) ? minY : layerMinY;
		layerMaxX = (maxX > layerMaxX) ? maxX : layerMaxX;                       
		layerMaxY = (maxY > layerMaxY) ? maxY : layerMaxY;
	}
	
	// Set the real layer bBox.
	this.bBox = new ia.BoundingBox(layerMinX, layerMinY, layerMaxX, layerMaxY);  
	
	// Check if layer uses an icon.
	if (this.iconPath !== "")
	{
		this.icon = new Image();
		this.icon.onload = function() {};
		this.icon.src = this.iconPath;
	}
};

/** 
 * Renders the layer. Keep this simple so that it can be overridden but
 * we can still use the draw method defined below.
 *
 * @method render
 */
ia.FeatureLayer.prototype.render = function() 
{
	if (this.map && this.getVisible() && this.isLoaded)
	{
		this.clear();
		this.draw();
	};
};

/** 
 * Draws the layer.
 *
 * @method draw
 */
ia.FeatureLayer.prototype.draw = function() 
{
	// Dont bother drawing if the bBox isnt set or the layer isnt visible.
	if (this.map && this.getVisible() && this.isLoaded)
	{
		// Reset the context styles in case the layer styles has changed.
		for (var p in this.style) 
		{
			this.context[p] = this.style[p];
			if (this.interactive)
			{
				this.selectionContext[p] = this.style[p];
				this.highlightContext[p] = this.style[p];
			}
		} 
		
		if (this.interactive)
		{
			this.selectionContext.strokeStyle = this.selectionColor;
			this.selectionContext.fillStyle = ia.Color.toRGBA(this.selectionColor, this.selectionOpacity);
			this.selectionContext.lineWidth = Math.max(2, parseFloat(this.style.lineWidth) + 1.5);
		
			this.highlightContext.strokeStyle = ia.Color.toRGBA(this.highlightColor, 0.8);
			this.highlightContext.fillStyle = ia.Color.toRGBA(this.highlightColor, this.highlightOpacity);
			this.highlightContext.lineWidth = Math.max(2, parseFloat(this.style.lineWidth) + 1.5);
		}
	
		// Get the data.
		var data = this.getData()

		// Dont bother drawing if its outside the map bBox.
		var mapBBox = this.map.getBBox();
		if (this.bBox.intersects(mapBBox))	
		{
			// Check display extent for labelling.
			var withinExtents = false;
			var minExtent = Math.min(mapBBox.getWidth(), mapBBox.getHeight());
			var maxExtent = Math.max(mapBBox.getWidth(), mapBBox.getHeight());
			if ((minExtent >= this.minLabelExtent) && (maxExtent <= this.maxLabelExtent)) withinExtents = true;
			
			var n = this.itemArray.length;
			for (var i = 0; i < n; i++) 
			{
				var item = this.itemArray[i];
				
				// Update data if needed.
				if (this.dataChanged)
				{
					var dataItem = data[item.id];
					if (dataItem)
					{
						item.name = dataItem.name;
						item.disabled = false;
						var alpha = ia.Color.a(this.style.fillStyle);
						if (dataItem.color) item.color = ia.Color.toRGBA(dataItem.color, alpha);
						item.symbolSize = dataItem.symbolSize;
					}
					else 
					{
						item.disabled = true;
						item.color = this.style.fillStyle;
						item.symbolSize = 0;
					}
				}	

				if (this.geometry !== "point")
				{
					if (item.bBox && item.bBox.intersects(mapBBox))   
					{
						this.renderItem(item);
						if (this.showLabels && withinExtents) this.renderItemLabel(item);
					}
				}
			}
		
			// Points get rendered after theyve been sorted by symbol size
			if (this.geometry === "point")
			{
				var dir = -1;
				this.itemArray.sort(function(a, b)
				{
					if (a.symbolSize < b.symbolSize) return -dir;
					if (a.symbolSize > b.symbolSize) return dir;
					return 0; 
				});
				var n = this.itemArray.length;
				for (var i = 0; i < n; i++) 
				{
					var item = this.itemArray[i];
					if (item.bBox && item.bBox.intersects(mapBBox) && item.symbolSize > 0)   
					{
						this.renderItem(item);
						if (this.showLabels && withinExtents) this.renderItemLabel(item);
					}
				}
			}

			if (this.dataChanged) this.dataChanged = false;
		}
		
		// Render selection.
		this.renderSelection();
	}
};

/**
 * Selects the item.
 *
 * @method selectItem
 * @param {Object} item The item.
 */
ia.FeatureLayer.prototype.selectItem = function(item) 
{
	this._drawItem(item, this.selectionContext);
};

/**
 * Highlights the item.
 *
 * @method highlightItem
 * @param {Object} item The item.
 */
ia.FeatureLayer.prototype.highlightItem = function(item) 
{
	this._drawItem(item, this.highlightContext);
};

/**
 * Renders the item to the given context.
 *
 * @method renderItem
 * @param {Object} item The item.
 */
ia.FeatureLayer.prototype.renderItem = function(item)
{
	if (item.color) 
	{
		this.context.fillStyle = item.color;
		if (this.geometry === "line") this.context.strokeStyle = item.color;
	}
	this._drawItem(item, this.context);
};

/**
 * Does the actual drawing.
 *
 * @method _drawItem
 * @param {Object} item The item.
 * @param {HTML Canvas Context} ctx The context to render to.
 * @private
 */
ia.FeatureLayer.prototype._drawItem = function(item, ctx)
{
	if (this.getVisible() && ctx)
	{
		ctx.beginPath();

			var nShapes = item.shapes.length;

			// Draw polygons.
			if (this.geometry === "polygon")
			{
				for (var j = 0; j < nShapes; j++) 
				{
					var coords = item.shapes[j];
					var nCoords = coords.length;

					ctx.moveTo(this.map.getPixelX(coords[0].x), this.map.getPixelY(coords[0].y));
					for (var i = 1; i < nCoords; i++) 
					{
						ctx.lineTo(this.map.getPixelX(coords[i].x), this.map.getPixelY(coords[i].y));
					}
				}
			}
			// Draw points.
			else if (this.geometry === "point")
			{
				for (var j = 0; j < nShapes; j++) 
				{
					var coords = item.shapes[j];
					var px = this.map.getPixelX(coords.x);
					var py = this.map.getPixelY(coords.y);

					if (this.icon !== undefined) 
						ctx.drawImage(this.icon, (px - (this.icon.width/2)), (py - (this.icon.height/2)), this.icon.width, this.icon.height);
					else 
						if (item.symbolSize > 0) ia.Shape.draw(this.symbol, ctx, px, py, item.symbolSize);
				}
			}
			// Draw lines.
			else if (this.geometry === "line")
			{
				if (item.symbolSize > 0) 
				{
					for (var j = 0; j < nShapes; j++) 
					{
						var coords = item.shapes[j];
						var nCoords = coords.length;

						ctx.moveTo(this.map.getPixelX(coords[0].x), this.map.getPixelY(coords[0].y));
						for (var i = 1; i < nCoords; i++) 
						{
							ctx.lineTo(this.map.getPixelX(coords[i].x), this.map.getPixelY(coords[i].y));
						}
					}
					ctx.lineWidth = item.symbolSize;
				}
			}

		if (this.geometry !== "line") ctx.fill();
		ctx.stroke();
	}
};

/**
 * Renders the item label.
 *
 * @method renderItemLabel
 * @param {Object} item The item.
 */
ia.FeatureLayer.prototype.renderItemLabel = function(item)
{	
	var x = this.map.getPixelX(item.bBox.getXCenter());
	var y = this.map.getPixelY(item.bBox.getYCenter());

	if (this.geometry === "polygon")
	{
		this.labelContext.textBaseline = 'middle';
		this.labelContext.textAlign = 'center';
	}
	else
	{
		this.labelContext.textBaseline = 'bottom';
		this.labelContext.textAlign = 'left';

		if (this.icon)
		{
			x = x + (this.icon.width / 2);
			y = y - (this.icon.width / 2);
		}
		else
		{
			x = x + (item.symbolSize / 2);
			y = y - (item.symbolSize / 2);
		}
	}

	this.labelContext.strokeText(item.name, x, y);
	this.labelContext.fillText(item.name, x, y);
};

//--------------------------------------------------------------------------
//
// Hit Test Methods
//
//--------------------------------------------------------------------------
	
/**
 * Runs a hit test on shapes. 
 * 
 * @method hitItem
 * @param {Object} item The item to hit test.
 * @param {ia.MapMouseEvent} event An <code>ia.MapMouseEvent</code>.
 */
ia.FeatureLayer.prototype.hitItem = function(item, event)
{
	// Convert to data coords.
	var pointX = event.dataX;
	var pointY = event.dataY;
	
	if (this.geometry === "point")
	{
		var points = item.shapes;
		var coords;

		for (var i = 0; i < points.length; i++) 
		{
			coords = points[i];    
			
			var isHit;
			if (this.iconPath !== "")
				isHit = this._pointInRect(this.icon.width, this.icon.height, coords, pointX, pointY);
			else
				isHit = this._pointInRect(item.symbolSize, item.symbolSize, coords, pointX, pointY);

			if (isHit) return true;
		}
	}
	else if (this.geometry === "polygon")
	{
		// Test bBox of item first.
		if (item.bBox)
		{
			if ((pointX < item.bBox.left()) || (pointX > item.bBox.right())) return false;
			else if ((pointY > item.bBox.top()) || (pointY < item.bBox.bottom())) return false;
		}

		var polys = item.shapes;

		for (var i = 0; i < polys.length; i++) 
		{
			var isHit = this._pointInPoly(polys[i], pointX, pointY);
			if (isHit) return true;
		}
	}
	else if (this.geometry === "line")
	{
		// Test bBox of item first.
		if (item.bBox)
		{
			if ((pointX < item.bBox.left()) || (pointX > item.bBox.right())) return false;
			else if ((pointY > item.bBox.top()) || (pointY < item.bBox.bottom())) return false;
		}
		
		var lines = item.shapes;

		for (var i = 0; i < lines.length; i++) 
		{
			var isHit = this._pointInLine(lines[i], event.x, event.y);
			if (isHit) return true;
		}
	}

	return false;
};

/** 
 * Query if a point lies completely within a polygon.
 *
 * @method _pointInLine
 * @param {Number[]} coords The coords to hit test.
 * @param {Number} pointX The pixel x coordinate of the test point.
 * @param {Number} pointY The pixel y coordinate of the test point.
 * @private
 */
ia.FeatureLayer.prototype._pointInLine = function(coords, pointX, pointY)
{
	var buffer = 4;
	var r = new ia.Rectangle(pointX - buffer, pointY - buffer, (buffer*2), (buffer*2))
	
	var n = coords.length;
	for (var i = 0; i < n - 1; i++) 
	{
		var x1 = this.map.getPixelX(coords[i].x);
		var y1 = this.map.getPixelY(coords[i].y);
		var x2 = this.map.getPixelX(coords[i+1].x);
		var y2 = this.map.getPixelY(coords[i+1].y);
		if (r.intersectsLine({x:x1,y:y1}, {x:x2,y:y2})) return true;
	}
	return false;
};

/** 
 * Query if a point lies completely within a polygon.
 *
 * @method _pointInPoly
 * @param {Number[]} coords The coords to hit test.
 * @param {Number} pointX The x coordinate of the test point.
 * @param {Number} pointY The y coordinate of the test point.
 * @private
 */
ia.FeatureLayer.prototype._pointInPoly = function(coords, pointX, pointY)
{
	var i, j, c = 0;
	for (i = 0, j = coords.length - 1; i < coords.length; j = i++)
	{
		if (((coords[i].y > pointY) !== (coords[j].y > pointY)) && (pointX < (coords[j].x - coords[i].x) * (pointY - coords[i].y) / (coords[j].y - coords[i].y) + coords[i].x))
		{
			c = !c;
		}
	}
	return c;
};

/** 
 * Query if a point lies completely within a shape.
 *
 * @method _pointInRect
 * @param {Number} symbolWidth The symbol width.
 * @param {Number} symbolHeight The symbol height.
 * @param {Number[]} coords The coords to hit test.
 * @param {Number} pointX The x coordinate of the test point.
 * @param {Number} pointY The y coordinate of the test point.
 * @private
 */
ia.FeatureLayer.prototype._pointInRect = function(symbolWidth, symbolHeight, coords, pointX, pointY)
{
	// Now test the polygon.
	var x = coords.x;
	var y = coords.y;
	
	var w = this.map.getDataWidth(symbolWidth);
	var h = this.map.getDataHeight(symbolHeight);
	
	var x1 = x - (w/2);
	var x2 = x + (w/2);
	var y1 = y - (h/2);
	var y2 = y + (h/2);
	
	if (pointX >= x1 &&  pointX <= x2 && pointY >= y1 &&  pointY <= y2) return true
	
	return false;
};

/** 
 * Displays the tip for the passed item
 * 
 * @method showTip
 * @param {Object} item The map item.	 
 * @param {ia.ItemEvent} event An <code>ia.ItemEvent</code>.
 */
ia.FeatureLayer.prototype.showTip = function(item, event)
{
	this.map.datatip.text(this.tipFunction(item));
	var px, py;

	if (ia.IS_TOUCH_DEVICE)
	{
		px = event.x - (this.map.datatip.getWidth() / 2);
		py = event.y - (this.map.datatip.getHeight() + 30);
	}
	else
	{
		if (this.geometry === "point")
		{
			px = this.map.getPixelX(item.bBox.getXCenter()) + item.symbolSize / 2;
			py = this.map.getPixelY(item.bBox.getYCenter()) - item.symbolSize / 2 - this.map.datatip.getHeight();
		}
		else		
		{
			px = event.x + 10;
			py = event.y - this.map.datatip.getHeight();
		}
	}
	this.map.datatip.position(px, py);
	this.map.datatip.show();
};