1 /**
  2  * Source: Mapping.js
  3  * Copyright (c) 2013-2014 Oculus Info Inc.
  4  * @fileOverview Aperture Mappings are used to define supply pipelines for visual
  5  * properties of layers.
  6  */
  7 
  8 /**
  9  * @namespace
 10  * @ignore
 11  * Ensure namespace exists
 12  */
 13 aperture = (
 14 /** @private */
 15 function(namespace) {
 16 
 17 	var util = aperture.util,
 18 		forEach = util.forEach;
 19 
 20 
 21 	namespace.Mapping = aperture.Class.extend( 'aperture.Mapping',
 22 	/** @lends aperture.Mapping# */
 23 	{
 24 		/**
 25 		 * @class A Mapping is responsible for mapping value(s) for a visual property
 26 		 * as a constant ({@link #asValue}) or {@link #from} a data source,
 27 		 * {@link #using} an optional map key. Layer Mappings are
 28 		 * accessed and defined by calling {@link aperture.Layer#map layer.map}.
 29 		 *
 30 		 * @constructs
 31 		 * @factoryMade
 32 		 * @extends aperture.Class
 33 		 */
 34 		init : function( property ) {
 35 			/**
 36 			 * The visual property to which this mapping pertains
 37 			 * @private
 38 			 */
 39 			this.property = property;
 40 
 41 			/**
 42 			 * @private
 43 			 */
 44 			this.filters = [];
 45 
 46 			/**
 47 			 * @private
 48 			 */
 49 			this.dataAccessor = undefined;
 50 
 51 			/**
 52 			 * @private
 53 			 */
 54 			this.transformation = undefined;
 55 		},
 56 
 57 		/**
 58 		 * Specifies that this mapping should not inherit
 59 		 * from parent mappings.
 60 		 *
 61 		 * @returns {aperture.Mapping}
 62 		 *      this mapping object
 63 		 */
 64 		only : function () {
 65 			if (!this.hasOwnProperty('filters')) {
 66 				this.filters = [];
 67 			}
 68 			if (!this.hasOwnProperty('dataAccessor')) {
 69 				this.dataAccessor = undefined;
 70 			}
 71 			if (!this.hasOwnProperty('transformation')) {
 72 				this.transformation = undefined;
 73 			}
 74 
 75 			return this;
 76 		},
 77 
 78 		/**
 79 		 * Maps the graphic property from a source of values from the data object.
 80 		 * A visual property may be mapped using one or more of the following constructs:
 81 		 * <ul>
 82 		 * <li>Field: A visual property may be mapped to a given field in the data.</li>
 83 		 * <li>Function: A visual property may be mapped to a function that will be called and provided
 84 		 * the data item and expected to return a value for the property.</li>
 85 		 * </ul>
 86 		 *
 87 		 * @example
 88 		 * // Map x to a field in the data object called 'xCoord'
 89 		 * layer.map('x').from('xCoord');
 90 		 * 
 91 		 * // Map label to the value returned by the given function
 92 		 * layer.map('label').from( function() { return 'Name: ' + this.name; } );
 93 		 * 
 94 		 * // Map label to the value returned by the given data object's prototype function
 95 		 * layer.map('label').from( MyDataType.prototype.getName );
 96 		 * 
 97 		 * // Map x to a sequence of values and count to a static value of 20
 98 		 * layer.map('x').from('xCoord[]');
 99 		 * 
100 		 * // Map y to a function and count to the length of the array field 'points'
101 		 * layer.map('y').from( function(data, index) { return points[index].y; } );
102 		 * layer.map('count').from('points.length');
103 		 *
104 		 * @param {String|Function} source
105 		 *      the source of the data to map the graphic property.  May be a function that
106 		 *      maps a given data object to the desired source data in the form
107 		 *      <code>function(dataObject)</code>, or may be a data object field name
108 		 *      in the form <code>'a.b.c'</code> where the data will be sourced from
109 		 *      <code>dataObject.a.b.c</code>.  The length of an array field may be mapped
110 		 *      using <code>'fieldName.length'</code>.
111 		 *
112 		 * @returns {aperture.Mapping}
113 		 *      this mapping object
114 		 */
115 		from : function( source ) {
116 			// Preprocess the source to determine if it's a function, field reference, or constant
117 			if( util.isFunction(source) ) {
118 				/**
119 				 * @private
120 				 * Given a function, use it as the mapping function straight up
121 				 */
122 				this.dataAccessor = source;
123 
124 			} else if( util.isString(source) ) {
125 				// Validate that this is a valid looking field definition
126 				var fieldChain = source.match(jsIdentifierRegEx);
127 				// Is a field definition?
128 				if( fieldChain ) {
129 					// Yes, create an array of field names in chain
130 					// Remove . from field names.  Leave []s
131 					fieldChain = util.map( fieldChain, function(field) {
132 						// Remove dots
133 						if( field.charAt(field.length-1) === '.' ) {
134 							return field.slice(0,field.length-1);
135 						} else {
136 							return field;
137 						}
138 					});
139 
140 					/**
141 					 * @private
142 					 * Create a function that dereferences the given data item down the
143 					 * calculated field chain
144 					 */
145 					this.dataAccessor = function() {
146 						// Make a clone since the array will be changed
147 						// TODO Hide this need to copy?
148 						var chain = fieldChain.slice();
149 						// Pass in array of arguments = array of indexes
150 						return findFieldChainValue.call( this, chain, Array.prototype.slice.call(arguments) );
151 					};
152 
153 					// TODO A faster version of the above for a single field
154 				} else {
155 					// String, but not a valid js field identifier
156 					// TODO logging
157 					throw new Error('Invalid object field "'+source+'" used for mapping');
158 				}
159 			} else {
160 				// Not a function, not a field
161 				// TODO log
162 				throw new Error('Mapping may only be done from a field name or a function');
163 			}
164 
165 			return this;
166 		},
167 
168 		/**
169 		 * Maps this property to a constant value.  The value may be a string, number, boolean
170 		 * array, or object.  A mapping to a constant value is an alternative to mapping do
171 		 * data using {@link #from}.
172 		 *
173 		 * @param {Object} value
174 		 *      The value to bind to this property.
175 		 *
176 		 * @returns {aperture.Mapping}
177 		 *      this mapping object
178 		 */
179 		asValue : function( value ) {
180 			/**
181 			 * @private
182 			 * Is just a static value string
183 			 */
184 			this.dataAccessor = function() {
185 				return value;
186 			};
187 
188 			return this;
189 		},
190 
191 		/**
192 		 * Provides a codified representational key for mapping between source data and the graphic
193 		 * property via a MapKey object. A MapKey object encapsulates the function of mapping from
194 		 * data value to graphic representation and the information necessary to express that mapping
195 		 * visually in a legend. Map keys can be created from Range objects, which describe
196 		 * the data range for a variable.
197 		 *
198 		 * A map key may be combined with a constant, field, or function provided data value source,
199 		 * providing the mapping from a variable source to visual property value for each data item, subject
200 		 * to any final filtering.
201 		 *
202 		 * The map key object will be used to translate the data value to an appropriate value
203 		 * for the visual property.  For example, it may map a numeric data value to a color.
204 		 *
205 		 * Calling this function without an argument returns the current map key, if any.
206 		 * 
207 		 * @param {aperture.MapKey} mapKey
208 		 *      The map key object to use in mapping data values to graphic property values.
209 		 *      Passing in null removes any existing key, leaving the source value untransformed,
210 		 *      subject to any final filtering.
211 		 *
212 		 * @returns {aperture.Mapping|aperture.MapKey}
213 		 *      this mapping object if setting the value, else the map key if getting.
214 		 */
215 		using : function( mapKey ) {
216 			if ( mapKey === undefined ) {
217 				return this.transformation;
218 			}
219 			this.transformation = mapKey;
220 
221 			return this;
222 		},
223 
224 		/**
225 		 * Applies a filter to this visual property, or clears all filters if no filter is supplied.
226 		 * A filter is applied after a visual value
227 		 * is calculated using the values passed into {@link #from}, {@link #asValue}, and
228 		 * {@link #using}.  Filters can be used to alter the visual value, for example, making
229 		 * a color brighter or overriding the stroke with on certain conditions.  A filter is a
230 		 * function in the form:
231 		 *
232 		 * @example
233 		 * function( value, etc... ) {
234 		 *     // value:  the visual value to be modified by the filter
235 		 *     // etc:    other values (such as indexes) passed in by the renderer
236 		 *     // this:   the data item to which this value pertains
237 		 *
238 		 *     return modifiedValue;
239 		 * }
240 		 *
241 		 * @param {Function} filter
242 		 *      A filter function in the form specified above, or nothing / null if clearing.
243 		 */
244 		filter : function( filter ) {
245 			if( filter ) {
246 				// only add to our own set of filters.
247 				if (!this.hasOwnProperty('filters')) {
248 					this.filters = [filter];
249 				} else {
250 					this.filters.push( filter );
251 				}
252 			} else {
253 				// Clear
254 				this.filters = [];
255 			}
256 
257 			return this;
258 		},
259 
260 		/**
261 		 * Removes a pre-existing filter, leaving any other filters intact.
262 		 *
263 		 * @param {Function} filter
264 		 *   A filter function to find and remove.
265 		 */
266 		filterWithout : function ( filter ) {
267 			this.filters = util.without(this.filters, filter);
268 		},
269 
270 		/**
271 		 * Retrieves the visual property value for the given dataItem and optional indices.
272 		 *
273 		 * @param {Object} dataItem
274 		 *   The data object to retrieve a value for, which will be the value of <code>this</code> 
275 		 *   if evaluation involves calling a {@link #from from} and / or {@link #filter filter}function. 
276 		 *
277 		 * @param {Array} [index] 
278 		 *   An optional array of indices
279 		 *
280 		 *
281 		 */
282 		valueFor : function( dataItem, index ) {
283 			var value;
284 
285 			// Get value (if no accessor, undefined)
286 			if( this.dataAccessor ) {
287 				// Get value from function, provide all arguments after dataItem
288 				value = this.dataAccessor.apply( dataItem, index || [] );
289 			}
290 
291 			return this.value( value, dataItem, index );
292 		},
293 
294 		/**
295 		 * Maps a raw value by transforming it and applying filters, returning
296 		 * a visual property value.
297 		 * 
298 		 * @param {Object} value
299 		 *   The source value to map. 
300 		 *   
301 		 * @param {Object} [context]
302 		 *   The optional context to supply to any filters. If omitted the value
303 		 *   of this in the filter call will be the Mapping instance.
304 		 *
305 		 * @param {Array} [index] 
306 		 *   Optional indices to pass to the filters.
307 		 *  
308 		 * @returns {Object}
309 		 *   A transformed and filtered value.
310 		 */
311 		value : function( value, context, index ) {
312 			
313 			// Transform
314 			if( this.transformation ) {
315 				value = this.transformation.map( value );
316 			}
317 
318 			return this.filteredValue( value, context, index );
319 		},
320 		
321 		/**
322 		 * @protected
323 		 * Execute the filter.
324 		 */
325 		filteredValue : function( value, context, index ) {
326 			
327 			// Filter
328 			if( this.filters.length ) {
329 				context = context || this;
330 				var args = [value].concat(index);
331 				
332 				forEach( this.filters, function(filter) {
333 					// Apply the filter
334 					value = filter.apply(context, args);
335 					// Update value in args for next filter
336 					args[0] = value;
337 				});
338 			}
339 
340 			return value;
341 		}
342 		
343 	});
344 
345 	return namespace;
346 
347 }(aperture || {}));
348