1 /**
  2  * Returns a geographic scale. The arguments to this constructor are optional,
  3  * and equivalent to calling {@link #projection}.
  4  *
  5  * @class Represents a geographic scale; a mapping between latitude-longitude
  6  * coordinates and screen pixel coordinates. By default, the domain is inferred
  7  * from the geographic coordinates, so that the domain fills the output range.
  8  *
  9  * <p>Note that geographic scales are two-dimensional transformations, rather
 10  * than the one-dimensional bidrectional mapping typical of other scales.
 11  * Rather than mapping (for example) between a numeric domain and a numeric
 12  * range, geographic scales map between two coordinate objects: {@link
 13  * pv.Geo.LatLng} and {@link pv.Vector}.
 14  *
 15  * @param {pv.Geo.Projection} [p] optional projection.
 16  * @see pv.Geo.scale#ticks
 17  */
 18 pv.Geo.scale = function(p) {
 19   var rmin = {x: 0, y: 0}, // default range minimum
 20       rmax = {x: 1, y: 1}, // default range maximum
 21       d = [], // default domain
 22       j = pv.Geo.projections.identity, // domain <-> normalized range
 23       x = pv.Scale.linear(-1, 1).range(0, 1), // normalized <-> range
 24       y = pv.Scale.linear(-1, 1).range(1, 0), // normalized <-> range
 25       c = {lng: 0, lat: 0}, // Center Point
 26       lastLatLng, // cached latlng
 27       lastPoint; // cached point
 28 
 29   /** @private */
 30   function scale(latlng) {
 31     if (!lastLatLng
 32         || (latlng.lng != lastLatLng.lng)
 33         || (latlng.lat != lastLatLng.lat)) {
 34       lastLatLng = latlng;
 35       var p = project(latlng);
 36       lastPoint = {x: x(p.x), y: y(p.y)};
 37     }
 38     return lastPoint;
 39   }
 40 
 41   /** @private */
 42   function project(latlng) {
 43     var offset = {lng: latlng.lng - c.lng, lat: latlng.lat};
 44     return j.project(offset);
 45   }
 46 
 47   /** @private */
 48   function invert(xy) {
 49     var latlng = j.invert(xy);
 50     latlng.lng += c.lng;
 51     return latlng;
 52   }
 53 
 54   /** Returns the projected x-coordinate. */
 55   scale.x = function(latlng) {
 56     return scale(latlng).x;
 57   };
 58 
 59   /** Returns the projected y-coordinate. */
 60   scale.y = function(latlng) {
 61     return scale(latlng).y;
 62   };
 63 
 64   /**
 65    * Abstract; this is a local namespace on a given geographic scale.
 66    *
 67    * @namespace Tick functions for geographic scales. Because geographic scales
 68    * represent two-dimensional transformations (as opposed to one-dimensional
 69    * transformations typical of other scales), the tick values are similarly
 70    * represented as two-dimensional coordinates in the input domain, i.e.,
 71    * {@link pv.Geo.LatLng} objects.
 72    *
 73    * <p>Also, note that non-rectilinear projections, such as sinsuoidal and
 74    * aitoff, may not produce straight lines for constant longitude or constant
 75    * latitude. Therefore the returned array of ticks is a two-dimensional array,
 76    * sampling various latitudes as constant longitude, and vice versa.
 77    *
 78    * <p>The tick lines can therefore be approximated as polylines, either with
 79    * "linear" or "cardinal" interpolation. This is not as accurate as drawing
 80    * the true curve through the projection space, but is usually sufficient.
 81    *
 82    * @name pv.Geo.scale.prototype.ticks
 83    * @see pv.Geo.scale
 84    * @see pv.Geo.LatLng
 85    * @see pv.Line#interpolate
 86    */
 87   scale.ticks = {
 88 
 89     /**
 90      * Returns longitude ticks.
 91      *
 92      * @function
 93      * @param {number} [m] the desired number of ticks.
 94      * @returns {array} a nested array of <tt>pv.Geo.LatLng</tt> ticks.
 95      * @name pv.Geo.scale.prototype.ticks.prototype.lng
 96      */
 97     lng: function(m) {
 98       var lat, lng;
 99       if (d.length > 1) {
100         var s = pv.Scale.linear();
101         if (m == undefined) m = 10;
102         lat = s.domain(d, function(d) { return d.lat; }).ticks(m);
103         lng = s.domain(d, function(d) { return d.lng; }).ticks(m);
104       } else {
105         lat = pv.range(-80, 81, 10);
106         lng = pv.range(-180, 181, 10);
107       }
108       return lng.map(function(lng) {
109         return lat.map(function(lat) {
110           return {lat: lat, lng: lng};
111         });
112       });
113     },
114 
115     /**
116      * Returns latitude ticks.
117      *
118      * @function
119      * @param {number} [m] the desired number of ticks.
120      * @returns {array} a nested array of <tt>pv.Geo.LatLng</tt> ticks.
121      * @name pv.Geo.scale.prototype.ticks.prototype.lat
122      */
123     lat: function(m) {
124       return pv.transpose(scale.ticks.lng(m));
125     }
126   };
127 
128   /**
129    * Inverts the specified value in the output range, returning the
130    * corresponding value in the input domain. This is frequently used to convert
131    * the mouse location (see {@link pv.Mark#mouse}) to a value in the input
132    * domain. Inversion is only supported for numeric ranges, and not colors.
133    *
134    * <p>Note that this method does not do any rounding or bounds checking. If
135    * the input domain is discrete (e.g., an array index), the returned value
136    * should be rounded. If the specified <tt>y</tt> value is outside the range,
137    * the returned value may be equivalently outside the input domain.
138    *
139    * @function
140    * @name pv.Geo.scale.prototype.invert
141    * @param {number} y a value in the output range (a pixel location).
142    * @returns {number} a value in the input domain.
143    */
144   scale.invert = function(p) {
145     return invert({x: x.invert(p.x), y: y.invert(p.y)});
146   };
147 
148   /**
149    * Sets or gets the input domain. Note that unlike quantitative scales, the
150    * domain cannot be reduced to a simple rectangle (i.e., minimum and maximum
151    * values for latitude and longitude). Instead, the domain values must be
152    * projected to normalized space, effectively finding the domain in normalized
153    * space rather than in terms of latitude and longitude. Thus, changing the
154    * projection requires recomputing the normalized domain.
155    *
156    * <p>This method can be invoked several ways:
157    *
158    * <p>1. <tt>domain(values...)</tt>
159    *
160    * <p>Specifying the domain as a series of {@link pv.Geo.LatLng}s is the most
161    * explicit and recommended approach. However, if the domain values are
162    * derived from data, you may find the second method more appropriate.
163    *
164    * <p>2. <tt>domain(array, f)</tt>
165    *
166    * <p>Rather than enumerating the domain explicitly, you can specify a single
167    * argument of an array. In addition, you can specify an optional accessor
168    * function to extract the domain values (as {@link pv.Geo.LatLng}s) from the
169    * array. If the specified array has fewer than two elements, this scale will
170    * default to the full normalized domain.
171    *
172    * <p>2. <tt>domain()</tt>
173    *
174    * <p>Invoking the <tt>domain</tt> method with no arguments returns the
175    * current domain as an array.
176    *
177    * @function
178    * @name pv.Geo.scale.prototype.domain
179    * @param {...} domain... domain values.
180    * @returns {pv.Geo.scale} <tt>this</tt>, or the current domain.
181    */
182   scale.domain = function(array, f) {
183     if (arguments.length) {
184       d = (array instanceof Array)
185           ? ((arguments.length > 1) ? pv.map(array, f) : array)
186           : Array.prototype.slice.call(arguments);
187       if (d.length > 1) {
188         var lngs = d.map(function(c) { return c.lng; });
189         var lats = d.map(function(c) { return c.lat; });
190         c = {
191           lng: (pv.max(lngs) + pv.min(lngs)) / 2,
192           lat: (pv.max(lats) + pv.min(lats)) / 2
193         };
194         var n = d.map(project); // normalized domain
195         x.domain(n, function(p) { return p.x; });
196         y.domain(n, function(p) { return p.y; });
197       } else {
198         c = {lng: 0, lat: 0};
199         x.domain(-1, 1);
200         y.domain(-1, 1);
201       }
202       lastLatLng = null; // invalidate the cache
203       return this;
204     }
205     return d;
206   };
207 
208   /**
209    * Sets or gets the output range. This method can be invoked several ways:
210    *
211    * <p>1. <tt>range(min, max)</tt>
212    *
213    * <p>If two objects are specified, the arguments should be {@link pv.Vector}s
214    * which specify the minimum and maximum values of the x- and y-coordinates
215    * explicitly.
216    *
217    * <p>2. <tt>range(width, height)</tt>
218    *
219    * <p>If two numbers are specified, the arguments specify the maximum values
220    * of the x- and y-coordinates explicitly; the minimum values are implicitly
221    * zero.
222    *
223    * <p>3. <tt>range()</tt>
224    *
225    * <p>Invoking the <tt>range</tt> method with no arguments returns the current
226    * range as an array of two {@link pv.Vector}s: the minimum (top-left) and
227    * maximum (bottom-right) values.
228    *
229    * @function
230    * @name pv.Geo.scale.prototype.range
231    * @param {...} range... range values.
232    * @returns {pv.Geo.scale} <tt>this</tt>, or the current range.
233    */
234   scale.range = function(min, max) {
235     if (arguments.length) {
236       if (typeof min == "object") {
237         rmin = {x: Number(min.x), y: Number(min.y)};
238         rmax = {x: Number(max.x), y: Number(max.y)};
239       } else {
240         rmin = {x: 0, y: 0};
241         rmax = {x: Number(min), y: Number(max)};
242       }
243       x.range(rmin.x, rmax.x);
244       y.range(rmax.y, rmin.y); // XXX flipped?
245       lastLatLng = null; // invalidate the cache
246       return this;
247     }
248     return [rmin, rmax];
249   };
250 
251   /**
252    * Sets or gets the projection. This method can be invoked several ways:
253    *
254    * <p>1. <tt>projection(string)</tt>
255    *
256    * <p>Specifying a string sets the projection to the given named projection in
257    * {@link pv.Geo.projections}. If no such projection is found, the identity
258    * projection is used.
259    *
260    * <p>2. <tt>projection(object)</tt>
261    *
262    * <p>Specifying an object sets the projection to the given custom projection,
263    * which must implement the <i>forward</i> and <i>inverse</i> methods per the
264    * {@link pv.Geo.Projection} interface.
265    *
266    * <p>3. <tt>projection()</tt>
267    *
268    * <p>Invoking the <tt>projection</tt> method with no arguments returns the
269    * current object that defined the projection.
270    *
271    * @function
272    * @name pv.Scale.geo.prototype.projection
273    * @param {...} range... range values.
274    * @returns {pv.Scale.geo} <tt>this</tt>, or the current range.
275    */
276   scale.projection = function(p) {
277     if (arguments.length) {
278       j = typeof p == "string"
279           ? pv.Geo.projections[p] || pv.Geo.projections.identity
280           : p;
281       return this.domain(d); // recompute normalized domain
282     }
283     return p;
284   };
285 
286   /**
287    * Returns a view of this scale by the specified accessor function <tt>f</tt>.
288    * Given a scale <tt>g</tt>, <tt>g.by(function(d) d.foo)</tt> is equivalent to
289    * <tt>function(d) g(d.foo)</tt>. This method should be used judiciously; it
290    * is typically more clear to invoke the scale directly, passing in the value
291    * to be scaled.
292    *
293    * @function
294    * @name pv.Geo.scale.prototype.by
295    * @param {function} f an accessor function.
296    * @returns {pv.Geo.scale} a view of this scale by the specified accessor
297    * function.
298    */
299   scale.by = function(f) {
300     function by() { return scale(f.apply(this, arguments)); }
301     for (var method in scale) by[method] = scale[method];
302     return by;
303   };
304 
305   if (arguments.length) scale.projection(p);
306   return scale;
307 };
308