Add .changes file
[profile/ivi/moute.git] / js / polymaps.js
1 if (!org) var org = {};
2 if (!org.polymaps) org.polymaps = {};
3 (function(po){
4
5   po.version = "2.5.0"; // semver.org
6
7   var zero = {x: 0, y: 0};
8 po.ns = {
9   svg: "http://www.w3.org/2000/svg",
10   xlink: "http://www.w3.org/1999/xlink"
11 };
12
13 function ns(name) {
14   var i = name.indexOf(":");
15   return i < 0 ? name : {
16     space: po.ns[name.substring(0, i)],
17     local: name.substring(i + 1)
18   };
19 }
20 po.id = (function() {
21   var id = 0;
22   return function() {
23     return ++id;
24   };
25 })();
26 po.svg = function(type) {
27   return document.createElementNS(po.ns.svg, type);
28 };
29 po.transform = function(a, b, c, d, e, f) {
30   var transform = {},
31       zoomDelta,
32       zoomFraction,
33       k;
34
35   if (!arguments.length) {
36     a = 1; c = 0; e = 0;
37     b = 0; d = 1; f = 0;
38   }
39
40   transform.zoomFraction = function(x) {
41     if (!arguments.length) return zoomFraction;
42     zoomFraction = x;
43     zoomDelta = Math.floor(zoomFraction + Math.log(Math.sqrt(a * a + b * b + c * c + d * d)) / Math.LN2);
44     k = Math.pow(2, -zoomDelta);
45     return transform;
46   };
47
48   transform.apply = function(x) {
49     var k0 = Math.pow(2, -x.zoom),
50         k1 = Math.pow(2, x.zoom - zoomDelta);
51     return {
52       column: (a * x.column * k0 + c * x.row * k0 + e) * k1,
53       row: (b * x.column * k0 + d * x.row * k0 + f) * k1,
54       zoom: x.zoom - zoomDelta
55     };
56   };
57
58   transform.unapply = function(x) {
59     var k0 = Math.pow(2, -x.zoom),
60         k1 = Math.pow(2, x.zoom + zoomDelta);
61     return {
62       column: (x.column * k0 * d - x.row * k0 * c - e * d + f * c) / (a * d - b * c) * k1,
63       row: (x.column * k0 * b - x.row * k0 * a - e * b + f * a) / (c * b - d * a) * k1,
64       zoom: x.zoom + zoomDelta
65     };
66   };
67
68   transform.toString = function() {
69     return "matrix(" + [a * k, b * k, c * k, d * k].join(" ") + " 0 0)";
70   };
71
72   return transform.zoomFraction(0);
73 };
74 po.cache = function(load, unload) {
75   var cache = {},
76       locks = {},
77       map = {},
78       head = null,
79       tail = null,
80       size = 64,
81       n = 0;
82
83   function remove(tile) {
84     n--;
85     if (unload) unload(tile);
86     delete map[tile.key];
87     if (tile.next) tile.next.prev = tile.prev;
88     else if (tail = tile.prev) tail.next = null;
89     if (tile.prev) tile.prev.next = tile.next;
90     else if (head = tile.next) head.prev = null;
91   }
92
93   function flush() {
94     for (var tile = tail; n > size; tile = tile.prev) {
95       if (!tile) break;
96       if (tile.lock) continue;
97       remove(tile);
98     }
99   }
100
101   cache.peek = function(c) {
102     return map[[c.zoom, c.column, c.row].join("/")];
103   };
104
105   cache.load = function(c, projection) {
106     var key = [c.zoom, c.column, c.row].join("/"),
107         tile = map[key];
108     if (tile) {
109       if (tile.prev) {
110         tile.prev.next = tile.next;
111         if (tile.next) tile.next.prev = tile.prev;
112         else tail = tile.prev;
113         tile.prev = null;
114         tile.next = head;
115         head.prev = tile;
116         head = tile;
117       }
118       tile.lock = 1;
119       locks[key] = tile;
120       return tile;
121     }
122     tile = {
123       key: key,
124       column: c.column,
125       row: c.row,
126       zoom: c.zoom,
127       next: head,
128       prev: null,
129       lock: 1
130     };
131     load.call(null, tile, projection);
132     locks[key] = map[key] = tile;
133     if (head) head.prev = tile;
134     else tail = tile;
135     head = tile;
136     n++;
137     return tile;
138   };
139
140   cache.unload = function(key) {
141     if (!(key in locks)) return false;
142     var tile = locks[key];
143     tile.lock = 0;
144     delete locks[key];
145     if (tile.request && tile.request.abort(false)) remove(tile);
146     return tile;
147   };
148
149   cache.locks = function() {
150     return locks;
151   };
152
153   cache.size = function(x) {
154     if (!arguments.length) return size;
155     size = x;
156     flush();
157     return cache;
158   };
159
160   cache.flush = function() {
161     flush();
162     return cache;
163   };
164
165   cache.clear = function() {
166     for (var key in map) {
167       var tile = map[key];
168       if (tile.request) tile.request.abort(false);
169       if (unload) unload(map[key]);
170       if (tile.lock) {
171         tile.lock = 0;
172         tile.element.parentNode.removeChild(tile.element);
173       }
174     }
175     locks = {};
176     map = {};
177     head = tail = null;
178     n = 0;
179     return cache;
180   };
181
182   return cache;
183 };
184 po.url = function(template) {
185   var hosts = [],
186       repeat = true;
187
188   function format(c) {
189     var max = c.zoom < 0 ? 1 : 1 << c.zoom,
190         column = c.column;
191     if (repeat) {
192       column = c.column % max;
193       if (column < 0) column += max;
194     } else if ((column < 0) || (column >= max)) {
195       return null;
196     }
197     return template.replace(/{(.)}/g, function(s, v) {
198       switch (v) {
199         case "S": return hosts[(Math.abs(c.zoom) + c.row + column) % hosts.length];
200         case "Z": return c.zoom;
201         case "X": return column;
202         case "Y": return c.row;
203         case "B": {
204           var nw = po.map.coordinateLocation({row: c.row, column: column, zoom: c.zoom}),
205               se = po.map.coordinateLocation({row: c.row + 1, column: column + 1, zoom: c.zoom}),
206               pn = Math.ceil(Math.log(c.zoom) / Math.LN2);
207           return se.lat.toFixed(pn)
208               + "," + nw.lon.toFixed(pn)
209               + "," + nw.lat.toFixed(pn)
210               + "," + se.lon.toFixed(pn);
211         }
212       }
213       return v;
214     });
215   }
216
217   format.template = function(x) {
218     if (!arguments.length) return template;
219     template = x;
220     return format;
221   };
222
223   format.hosts = function(x) {
224     if (!arguments.length) return hosts;
225     hosts = x;
226     return format;
227   };
228
229   format.repeat = function(x) {
230     if (!arguments.length) return repeat;
231     repeat = x;
232     return format;
233   };
234
235   return format;
236 };
237 po.dispatch = function(that) {
238   var types = {};
239
240   that.on = function(type, handler) {
241     var listeners = types[type] || (types[type] = []);
242     for (var i = 0; i < listeners.length; i++) {
243       if (listeners[i].handler == handler) return that; // already registered
244     }
245     listeners.push({handler: handler, on: true});
246     return that;
247   };
248
249   that.off = function(type, handler) {
250     var listeners = types[type];
251     if (listeners) for (var i = 0; i < listeners.length; i++) {
252       var l = listeners[i];
253       if (l.handler == handler) {
254         l.on = false;
255         listeners.splice(i, 1);
256         break;
257       }
258     }
259     return that;
260   };
261
262   return function(event) {
263     var listeners = types[event.type];
264     if (!listeners) return;
265     listeners = listeners.slice(); // defensive copy
266     for (var i = 0; i < listeners.length; i++) {
267       var l = listeners[i];
268       if (l.on) l.handler.call(that, event);
269     }
270   };
271 };
272 po.queue = (function() {
273   var queued = [], active = 0, size = 6;
274
275   function process() {
276     if ((active >= size) || !queued.length) return;
277     active++;
278     queued.pop()();
279   }
280
281   function dequeue(send) {
282     for (var i = 0; i < queued.length; i++) {
283       if (queued[i] == send) {
284         queued.splice(i, 1);
285         return true;
286       }
287     }
288     return false;
289   }
290
291   function request(url, callback, mimeType) {
292     var req;
293
294     function send() {
295       req = new XMLHttpRequest();
296       if (mimeType) {
297         req.overrideMimeType(mimeType);
298       }
299       req.open("GET", url, true);
300       req.onreadystatechange = function(e) {
301         if (req.readyState == 4) {
302           active--;
303           if (req.status < 300) callback(req);
304           process();
305         }
306       };
307       req.send(null);
308     }
309
310     function abort(hard) {
311       if (dequeue(send)) return true;
312       if (hard && req) { req.abort(); return true; }
313       return false;
314     }
315
316     queued.push(send);
317     process();
318     return {abort: abort};
319   }
320
321   function text(url, callback, mimeType) {
322     return request(url, function(req) {
323       if (req.responseText) callback(req.responseText);
324     }, mimeType);
325   }
326
327   /*
328    * We the override MIME type here so that you can load local files; some
329    * browsers don't assign a proper MIME type for local files.
330    */
331
332   function json(url, callback) {
333     return request(url, function(req) {
334       if (req.responseText) callback(JSON.parse(req.responseText));
335     }, "application/json");
336   }
337
338   function xml(url, callback) {
339     return request(url, function(req) {
340       if (req.responseXML) callback(req.responseXML);
341     }, "application/xml");
342   }
343
344   function image(image, src, callback) {
345     var img;
346
347     function send() {
348       img = document.createElement("img");
349       img.onerror = function() {
350         active--;
351         process();
352       };
353       img.onload = function() {
354         active--;
355         callback(img);
356         process();
357       };
358       img.src = src;
359       image.setAttributeNS(po.ns.xlink, "href", src);
360     }
361
362     function abort(hard) {
363       if (dequeue(send)) return true;
364       if (hard && img) { img.src = "about:"; return true; } // cancels request
365       return false;
366     }
367
368     queued.push(send);
369     process();
370     return {abort: abort};
371   }
372
373   return {text: text, xml: xml, json: json, image: image};
374 })();
375 po.map = function() {
376   var map = {},
377       container,
378       size,
379       sizeActual = zero,
380       sizeRadius = zero, // sizeActual / 2
381       tileSize = {x: 256, y: 256},
382       center = {lat: 37.76487, lon: -122.41948},
383       zoom = 12,
384       zoomFraction = 0,
385       zoomFactor = 1, // Math.pow(2, zoomFraction)
386       zoomRange = [1, 18],
387       angle = 0,
388       angleCos = 1, // Math.cos(angle)
389       angleSin = 0, // Math.sin(angle)
390       angleCosi = 1, // Math.cos(-angle)
391       angleSini = 0, // Math.sin(-angle)
392       ymin = -180, // lat2y(centerRange[0].lat)
393       ymax = 180; // lat2y(centerRange[1].lat)
394
395   var centerRange = [
396     {lat: y2lat(ymin), lon: -Infinity},
397     {lat: y2lat(ymax), lon: Infinity}
398   ];
399
400   map.locationCoordinate = function(l) {
401     var c = po.map.locationCoordinate(l),
402         k = Math.pow(2, zoom);
403     c.column *= k;
404     c.row *= k;
405     c.zoom += zoom;
406     return c;
407   };
408
409   map.coordinateLocation = po.map.coordinateLocation;
410
411   map.coordinatePoint = function(tileCenter, c) {
412     var kc = Math.pow(2, zoom - c.zoom),
413         kt = Math.pow(2, zoom - tileCenter.zoom),
414         dx = (c.column * kc - tileCenter.column * kt) * tileSize.x * zoomFactor,
415         dy = (c.row * kc - tileCenter.row * kt) * tileSize.y * zoomFactor;
416     return {
417       x: sizeRadius.x + angleCos * dx - angleSin * dy,
418       y: sizeRadius.y + angleSin * dx + angleCos * dy
419     };
420   };
421
422   map.pointCoordinate = function(tileCenter, p) {
423     var kt = Math.pow(2, zoom - tileCenter.zoom),
424         dx = (p.x - sizeRadius.x) / zoomFactor,
425         dy = (p.y - sizeRadius.y) / zoomFactor;
426     return {
427       column: tileCenter.column * kt + (angleCosi * dx - angleSini * dy) / tileSize.x,
428       row: tileCenter.row * kt + (angleSini * dx + angleCosi * dy) / tileSize.y,
429       zoom: zoom
430     };
431   };
432
433   map.locationPoint = function(l) {
434     var k = Math.pow(2, zoom + zoomFraction - 3) / 45,
435         dx = (l.lon - center.lon) * k * tileSize.x,
436         dy = (lat2y(center.lat) - lat2y(l.lat)) * k * tileSize.y;
437     return {
438       x: sizeRadius.x + angleCos * dx - angleSin * dy,
439       y: sizeRadius.y + angleSin * dx + angleCos * dy
440     };
441   };
442
443   map.pointLocation = function(p) {
444     var k = 45 / Math.pow(2, zoom + zoomFraction - 3),
445         dx = (p.x - sizeRadius.x) * k,
446         dy = (p.y - sizeRadius.y) * k;
447     return {
448       lon: center.lon + (angleCosi * dx - angleSini * dy) / tileSize.x,
449       lat: y2lat(lat2y(center.lat) - (angleSini * dx + angleCosi * dy) / tileSize.y)
450     };
451   };
452
453   function rezoom() {
454     if (zoomRange) {
455       if (zoom < zoomRange[0]) zoom = zoomRange[0];
456       else if (zoom > zoomRange[1]) zoom = zoomRange[1];
457     }
458     zoomFraction = zoom - (zoom = Math.round(zoom));
459     zoomFactor = Math.pow(2, zoomFraction);
460   }
461
462   function recenter() {
463     if (!centerRange) return;
464     var k = 45 / Math.pow(2, zoom + zoomFraction - 3);
465
466     // constrain latitude
467     var y = Math.max(Math.abs(angleSin * sizeRadius.x + angleCos * sizeRadius.y),
468                      Math.abs(angleSini * sizeRadius.x + angleCosi * sizeRadius.y)),
469         lat0 = y2lat(ymin - y * k / tileSize.y),
470         lat1 = y2lat(ymax + y * k / tileSize.y);
471     center.lat = Math.max(lat0, Math.min(lat1, center.lat));
472
473     // constrain longitude
474     var x = Math.max(Math.abs(angleSin * sizeRadius.y + angleCos * sizeRadius.x),
475                      Math.abs(angleSini * sizeRadius.y + angleCosi * sizeRadius.x)),
476         lon0 = centerRange[0].lon - x * k / tileSize.x,
477         lon1 = centerRange[1].lon + x * k / tileSize.x;
478     center.lon = Math.max(lon0, Math.min(lon1, center.lon));
479  }
480
481   // a place to capture mouse events if no tiles exist
482   var rect = po.svg("rect");
483   rect.setAttribute("visibility", "hidden");
484   rect.setAttribute("pointer-events", "all");
485
486   map.container = function(x) {
487     if (!arguments.length) return container;
488     container = x;
489     container.setAttribute("class", "map");
490     container.appendChild(rect);
491     return map.resize(); // infer size
492   };
493
494   map.focusableParent = function() {
495     for (var p = container; p; p = p.parentNode) {
496       if (p.tabIndex >= 0) return p;
497     }
498     return window;
499   };
500
501   map.mouse = function(e) {
502     var point = (container.ownerSVGElement || container).createSVGPoint();
503     if ((bug44083 < 0) && (window.scrollX || window.scrollY)) {
504       var svg = document.body.appendChild(po.svg("svg"));
505       svg.style.position = "absolute";
506       svg.style.top = svg.style.left = "0px";
507       var ctm = svg.getScreenCTM();
508       bug44083 = !(ctm.f || ctm.e);
509       document.body.removeChild(svg);
510     }
511     if (bug44083) {
512       point.x = e.pageX;
513       point.y = e.pageY;
514     } else {
515       point.x = e.clientX;
516       point.y = e.clientY;
517     }
518     return point.matrixTransform(container.getScreenCTM().inverse());
519   };
520
521   map.size = function(x) {
522     if (!arguments.length) return sizeActual;
523     size = x;
524     return map.resize(); // size tiles
525   };
526
527   map.resize = function() {
528     if (!size) {
529       rect.setAttribute("width", "100%");
530       rect.setAttribute("height", "100%");
531       b = rect.getBBox();
532       sizeActual = {x: b.width, y: b.height};
533       resizer.add(map);
534     } else {
535       sizeActual = size;
536       resizer.remove(map);
537     }
538     rect.setAttribute("width", sizeActual.x);
539     rect.setAttribute("height", sizeActual.y);
540     sizeRadius = {x: sizeActual.x / 2, y: sizeActual.y / 2};
541     recenter();
542     map.dispatch({type: "resize"});
543     return map;
544   };
545
546   map.tileSize = function(x) {
547     if (!arguments.length) return tileSize;
548     tileSize = x;
549     map.dispatch({type: "move"});
550     return map;
551   };
552
553   map.center = function(x) {
554     if (!arguments.length) return center;
555     center = x;
556     recenter();
557     map.dispatch({type: "move"});
558     return map;
559   };
560
561   map.panBy = function(x) {
562     var k = 45 / Math.pow(2, zoom + zoomFraction - 3),
563         dx = x.x * k,
564         dy = x.y * k;
565     return map.center({
566       lon: center.lon + (angleSini * dy - angleCosi * dx) / tileSize.x,
567       lat: y2lat(lat2y(center.lat) + (angleSini * dx + angleCosi * dy) / tileSize.y)
568     });
569   };
570
571   map.centerRange = function(x) {
572     if (!arguments.length) return centerRange;
573     centerRange = x;
574     if (centerRange) {
575       ymin = centerRange[0].lat > -90 ? lat2y(centerRange[0].lat) : -Infinity;
576       ymax = centerRange[0].lat < 90 ? lat2y(centerRange[1].lat) : Infinity;
577     } else {
578       ymin = -Infinity;
579       ymax = Infinity;
580     }
581     recenter();
582     map.dispatch({type: "move"});
583     return map;
584   };
585
586   map.zoom = function(x) {
587     if (!arguments.length) return zoom + zoomFraction;
588     zoom = x;
589     rezoom();
590     return map.center(center);
591   };
592
593   map.zoomBy = function(z, x0, l) {
594     if (arguments.length < 2) return map.zoom(zoom + zoomFraction + z);
595
596     // compute the location of x0
597     if (arguments.length < 3) l = map.pointLocation(x0);
598
599     // update the zoom level
600     zoom = zoom + zoomFraction + z;
601     rezoom();
602
603     // compute the new point of the location
604     var x1 = map.locationPoint(l);
605
606     return map.panBy({x: x0.x - x1.x, y: x0.y - x1.y});
607   };
608
609   map.zoomRange = function(x) {
610     if (!arguments.length) return zoomRange;
611     zoomRange = x;
612     return map.zoom(zoom + zoomFraction);
613   };
614
615   map.extent = function(x) {
616     if (!arguments.length) return [
617       map.pointLocation({x: 0, y: sizeActual.y}),
618       map.pointLocation({x: sizeActual.x, y: 0})
619     ];
620
621     // compute the extent in points, scale factor, and center
622     var bl = map.locationPoint(x[0]),
623         tr = map.locationPoint(x[1]),
624         k = Math.max((tr.x - bl.x) / sizeActual.x, (bl.y - tr.y) / sizeActual.y),
625         l = map.pointLocation({x: (bl.x + tr.x) / 2, y: (bl.y + tr.y) / 2});
626
627     // update the zoom level
628     zoom = zoom + zoomFraction - Math.log(k) / Math.LN2;
629     rezoom();
630
631     // set the new center
632     return map.center(l);
633   };
634
635   map.angle = function(x) {
636     if (!arguments.length) return angle;
637     angle = x;
638     angleCos = Math.cos(angle);
639     angleSin = Math.sin(angle);
640     angleCosi = Math.cos(-angle);
641     angleSini = Math.sin(-angle);
642     recenter();
643     map.dispatch({type: "move"});
644     return map;
645   };
646
647   map.add = function(x) {
648     x.map(map);
649     return map;
650   };
651
652   map.remove = function(x) {
653     x.map(null);
654     return map;
655   };
656
657   map.dispatch = po.dispatch(map);
658
659   return map;
660 };
661
662 function resizer(e) {
663   for (var i = 0; i < resizer.maps.length; i++) {
664     resizer.maps[i].resize();
665   }
666 }
667
668 resizer.maps = [];
669
670 resizer.add = function(map) {
671   for (var i = 0; i < resizer.maps.length; i++) {
672     if (resizer.maps[i] == map) return;
673   }
674   resizer.maps.push(map);
675 };
676
677 resizer.remove = function(map) {
678   for (var i = 0; i < resizer.maps.length; i++) {
679     if (resizer.maps[i] == map) {
680       resizer.maps.splice(i, 1);
681       return;
682     }
683   }
684 };
685
686 // Note: assumes single window (no frames, iframes, etc.)!
687 window.addEventListener("resize", resizer, false);
688 window.addEventListener("load", resizer, false);
689
690 // See http://wiki.openstreetmap.org/wiki/Mercator
691
692 function y2lat(y) {
693   return 360 / Math.PI * Math.atan(Math.exp(y * Math.PI / 180)) - 90;
694 }
695
696 function lat2y(lat) {
697   return 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360));
698 }
699
700 po.map.locationCoordinate = function(l) {
701   var k = 1 / 360;
702   return {
703     column: (l.lon + 180) * k,
704     row: (180 - lat2y(l.lat)) * k,
705     zoom: 0
706   };
707 };
708
709 po.map.coordinateLocation = function(c) {
710   var k = 45 / Math.pow(2, c.zoom - 3);
711   return {
712     lon: k * c.column - 180,
713     lat: y2lat(180 - k * c.row)
714   };
715 };
716
717 // https://bugs.webkit.org/show_bug.cgi?id=44083
718 var bug44083 = /WebKit/.test(navigator.userAgent) ? -1 : 0;
719 po.layer = function(load, unload) {
720   var layer = {},
721       cache = layer.cache = po.cache(load, unload).size(512),
722       tile = true,
723       visible = true,
724       zoom,
725       id,
726       map,
727       container = po.svg("g"),
728       transform,
729       levelZoom,
730       levels = {};
731
732   container.setAttribute("class", "layer");
733   for (var i = -4; i <= -1; i++) levels[i] = container.appendChild(po.svg("g"));
734   for (var i = 2; i >= 1; i--) levels[i] = container.appendChild(po.svg("g"));
735   levels[0] = container.appendChild(po.svg("g"));
736
737   function zoomIn(z) {
738     var end = levels[0].nextSibling;
739     for (; levelZoom < z; levelZoom++) {
740       // -4, -3, -2, -1, +2, +1, =0 // current order
741       // -3, -2, -1, +2, +1, =0, -4 // insertBefore(-4, end)
742       // -3, -2, -1, +1, =0, -4, +2 // insertBefore(+2, end)
743       // -3, -2, -1, =0, -4, +2, +1 // insertBefore(+1, end)
744       // -4, -3, -2, -1, +2, +1, =0 // relabel
745       container.insertBefore(levels[-4], end);
746       container.insertBefore(levels[2], end);
747       container.insertBefore(levels[1], end);
748       var t = levels[-4];
749       for (var dz = -4; dz < 2;) levels[dz] = levels[++dz];
750       levels[dz] = t;
751     }
752   }
753
754   function zoomOut(z) {
755     var end = levels[0].nextSibling;
756     for (; levelZoom > z; levelZoom--) {
757       // -4, -3, -2, -1, +2, +1, =0 // current order
758       // -4, -3, -2, +2, +1, =0, -1 // insertBefore(-1, end)
759       // +2, -4, -3, -2, +1, =0, -1 // insertBefore(+2, -4)
760       // -4, -3, -2, -1, +2, +1, =0 // relabel
761       container.insertBefore(levels[-1], end);
762       container.insertBefore(levels[2], levels[-4]);
763       var t = levels[2];
764       for (var dz = 2; dz > -4;) levels[dz] = levels[--dz];
765       levels[dz] = t;
766     }
767   }
768
769   function move() {
770     var map = layer.map(), // in case the layer is removed
771         mapZoom = map.zoom(),
772         mapZoomFraction = mapZoom - (mapZoom = Math.round(mapZoom)),
773         mapSize = map.size(),
774         mapAngle = map.angle(),
775         tileSize = map.tileSize(),
776         tileCenter = map.locationCoordinate(map.center());
777
778     // set the layer zoom levels
779     if (levelZoom != mapZoom) {
780       if (levelZoom < mapZoom) zoomIn(mapZoom);
781       else if (levelZoom > mapZoom) zoomOut(mapZoom);
782       else levelZoom = mapZoom;
783       for (var z = -4; z <= 2; z++) {
784         var l = levels[z];
785         l.setAttribute("class", "zoom" + (z < 0 ? "" : "+") + z + " zoom" + (mapZoom + z));
786         l.setAttribute("transform", "scale(" + Math.pow(2, -z) + ")");
787       }
788     }
789
790     // set the layer transform
791     container.setAttribute("transform",
792         "translate(" + (mapSize.x / 2) + "," + (mapSize.y / 2) + ")"
793         + (mapAngle ? "rotate(" + mapAngle / Math.PI * 180 + ")" : "")
794         + (mapZoomFraction ? "scale(" + Math.pow(2, mapZoomFraction) + ")" : "")
795         + (transform ? transform.zoomFraction(mapZoomFraction) : ""));
796
797     // get the coordinates of the four corners
798     var c0 = map.pointCoordinate(tileCenter, zero),
799         c1 = map.pointCoordinate(tileCenter, {x: mapSize.x, y: 0}),
800         c2 = map.pointCoordinate(tileCenter, mapSize),
801         c3 = map.pointCoordinate(tileCenter, {x: 0, y: mapSize.y});
802
803     // round to pixel boundary to avoid anti-aliasing artifacts
804     if (!mapZoomFraction && !mapAngle && !transform) {
805       tileCenter.column = (Math.round(tileSize.x * tileCenter.column) + (mapSize.x & 1) / 2) / tileSize.x;
806       tileCenter.row = (Math.round(tileSize.y * tileCenter.row) + (mapSize.y & 1) / 2) / tileSize.y;
807     }
808
809     // layer-specific coordinate transform
810     if (transform) {
811       c0 = transform.unapply(c0);
812       c1 = transform.unapply(c1);
813       c2 = transform.unapply(c2);
814       c3 = transform.unapply(c3);
815       tileCenter = transform.unapply(tileCenter);
816     }
817
818     // layer-specific zoom transform
819     var tileLevel = zoom ? zoom(c0.zoom) - c0.zoom : 0;
820     if (tileLevel) {
821       var k = Math.pow(2, tileLevel);
822       c0.column *= k; c0.row *= k;
823       c1.column *= k; c1.row *= k;
824       c2.column *= k; c2.row *= k;
825       c3.column *= k; c3.row *= k;
826       c0.zoom = c1.zoom = c2.zoom = c3.zoom += tileLevel;
827     }
828
829     // tile-specific projection
830     function projection(c) {
831       var zoom = c.zoom,
832           max = zoom < 0 ? 1 : 1 << zoom,
833           column = c.column % max,
834           row = c.row;
835       if (column < 0) column += max;
836       return {
837         locationPoint: function(l) {
838           var c = po.map.locationCoordinate(l),
839               k = Math.pow(2, zoom - c.zoom);
840           return {
841             x: tileSize.x * (k * c.column - column),
842             y: tileSize.y * (k * c.row - row)
843           };
844         }
845       };
846     }
847
848     // record which tiles are visible
849     var oldLocks = cache.locks(), newLocks = {};
850
851     // reset the proxy counts
852     for (var key in oldLocks) {
853       oldLocks[key].proxyCount = 0;
854     }
855
856     // load the tiles!
857     if (visible && tileLevel > -5 && tileLevel < 3) {
858       var ymax = c0.zoom < 0 ? 1 : 1 << c0.zoom;
859       if (tile) {
860         scanTriangle(c0, c1, c2, 0, ymax, scanLine);
861         scanTriangle(c2, c3, c0, 0, ymax, scanLine);
862       } else {
863         var x = Math.floor((c0.column + c2.column) / 2),
864             y = Math.max(0, Math.min(ymax - 1, Math.floor((c1.row + c3.row) / 2))),
865             z = Math.min(4, c0.zoom);
866         x = x >> z << z;
867         y = y >> z << z;
868         scanLine(x, x + 1, y);
869       }
870     }
871
872     // scan-line conversion
873     function scanLine(x0, x1, y) {
874       var z = c0.zoom,
875           z0 = 2 - tileLevel,
876           z1 = 4 + tileLevel;
877
878       for (var x = x0; x < x1; x++) {
879         var t = cache.load({column: x, row: y, zoom: z}, projection);
880         if (!t.ready && !(t.key in newLocks)) {
881           t.proxyRefs = {};
882           var c, full, proxy;
883
884           // downsample high-resolution tiles
885           for (var dz = 1; dz <= z0; dz++) {
886             full = true;
887             for (var dy = 0, k = 1 << dz; dy <= k; dy++) {
888               for (var dx = 0; dx <= k; dx++) {
889                 proxy = cache.peek(c = {
890                   column: (x << dz) + dx,
891                   row: (y << dz) + dy,
892                   zoom: z + dz
893                 });
894                 if (proxy && proxy.ready) {
895                   newLocks[proxy.key] = cache.load(c);
896                   proxy.proxyCount++;
897                   t.proxyRefs[proxy.key] = proxy;
898                 } else {
899                   full = false;
900                 }
901               }
902             }
903             if (full) break;
904           }
905
906           // upsample low-resolution tiles
907           if (!full) {
908             for (var dz = 1; dz <= z1; dz++) {
909               proxy = cache.peek(c = {
910                 column: x >> dz,
911                 row: y >> dz,
912                 zoom: z - dz
913               });
914               if (proxy && proxy.ready) {
915                 newLocks[proxy.key] = cache.load(c);
916                 proxy.proxyCount++;
917                 t.proxyRefs[proxy.key] = proxy;
918                 break;
919               }
920             }
921           }
922         }
923         newLocks[t.key] = t;
924       }
925     }
926
927     // position tiles
928     for (var key in newLocks) {
929       var t = newLocks[key],
930           k = Math.pow(2, t.level = t.zoom - tileCenter.zoom);
931       t.element.setAttribute("transform", "translate("
932         + (t.x = tileSize.x * (t.column - tileCenter.column * k)) + ","
933         + (t.y = tileSize.y * (t.row - tileCenter.row * k)) + ")");
934     }
935
936     // remove tiles that are no longer visible
937     for (var key in oldLocks) {
938       if (!(key in newLocks)) {
939         var t = cache.unload(key);
940         t.element.parentNode.removeChild(t.element);
941         delete t.proxyRefs;
942       }
943     }
944
945     // append tiles that are now visible
946     for (var key in newLocks) {
947       var t = newLocks[key];
948       if (t.element.parentNode != levels[t.level]) {
949         levels[t.level].appendChild(t.element);
950         if (layer.show) layer.show(t);
951       }
952     }
953
954     // flush the cache, clearing no-longer-needed tiles
955     cache.flush();
956
957     // dispatch the move event
958     layer.dispatch({type: "move"});
959   }
960
961   // remove proxy tiles when tiles load
962   function cleanup(e) {
963     if (e.tile.proxyRefs) {
964       for (var proxyKey in e.tile.proxyRefs) {
965         var proxyTile = e.tile.proxyRefs[proxyKey];
966         if ((--proxyTile.proxyCount <= 0) && cache.unload(proxyKey)) {
967           proxyTile.element.parentNode.removeChild(proxyTile.element);
968         }
969       }
970       delete e.tile.proxyRefs;
971     }
972   }
973
974   layer.map = function(x) {
975     if (!arguments.length) return map;
976     if (map) {
977       if (map == x) {
978         container.parentNode.appendChild(container); // move to end
979         return layer;
980       }
981       map.off("move", move).off("resize", move);
982       container.parentNode.removeChild(container);
983     }
984     map = x;
985     if (map) {
986       map.container().appendChild(container);
987       if (layer.init) layer.init(container);
988       map.on("move", move).on("resize", move);
989       move();
990     }
991     return layer;
992   };
993
994   layer.container = function() {
995     return container;
996   };
997
998   layer.levels = function() {
999     return levels;
1000   };
1001
1002   layer.id = function(x) {
1003     if (!arguments.length) return id;
1004     id = x;
1005     container.setAttribute("id", x);
1006     return layer;
1007   };
1008
1009   layer.visible = function(x) {
1010     if (!arguments.length) return visible;
1011     if (visible = x) container.removeAttribute("visibility")
1012     else container.setAttribute("visibility", "hidden");
1013     if (map) move();
1014     return layer;
1015   };
1016
1017   layer.transform = function(x) {
1018     if (!arguments.length) return transform;
1019     transform = x;
1020     if (map) move();
1021     return layer;
1022   };
1023
1024   layer.zoom = function(x) {
1025     if (!arguments.length) return zoom;
1026     zoom = typeof x == "function" || x == null ? x : function() { return x; };
1027     if (map) move();
1028     return layer;
1029   };
1030
1031   layer.tile = function(x) {
1032     if (!arguments.length) return tile;
1033     tile = x;
1034     if (map) move();
1035     return layer;
1036   };
1037
1038   layer.reload = function() {
1039     cache.clear();
1040     if (map) move();
1041     return layer;
1042   };
1043
1044   layer.dispatch = po.dispatch(layer);
1045   layer.on("load", cleanup);
1046
1047   return layer;
1048 };
1049
1050 // scan-line conversion
1051 function edge(a, b) {
1052   if (a.row > b.row) { var t = a; a = b; b = t; }
1053   return {
1054     x0: a.column,
1055     y0: a.row,
1056     x1: b.column,
1057     y1: b.row,
1058     dx: b.column - a.column,
1059     dy: b.row - a.row
1060   };
1061 }
1062
1063 // scan-line conversion
1064 function scanSpans(e0, e1, ymin, ymax, scanLine) {
1065   var y0 = Math.max(ymin, Math.floor(e1.y0)),
1066       y1 = Math.min(ymax, Math.ceil(e1.y1));
1067
1068   // sort edges by x-coordinate
1069   if ((e0.x0 == e1.x0 && e0.y0 == e1.y0)
1070       ? (e0.x0 + e1.dy / e0.dy * e0.dx < e1.x1)
1071       : (e0.x1 - e1.dy / e0.dy * e0.dx < e1.x0)) {
1072     var t = e0; e0 = e1; e1 = t;
1073   }
1074
1075   // scan lines!
1076   var m0 = e0.dx / e0.dy,
1077       m1 = e1.dx / e1.dy,
1078       d0 = e0.dx > 0, // use y + 1 to compute x0
1079       d1 = e1.dx < 0; // use y + 1 to compute x1
1080   for (var y = y0; y < y1; y++) {
1081     var x0 = m0 * Math.max(0, Math.min(e0.dy, y + d0 - e0.y0)) + e0.x0,
1082         x1 = m1 * Math.max(0, Math.min(e1.dy, y + d1 - e1.y0)) + e1.x0;
1083     scanLine(Math.floor(x1), Math.ceil(x0), y);
1084   }
1085 }
1086
1087 // scan-line conversion
1088 function scanTriangle(a, b, c, ymin, ymax, scanLine) {
1089   var ab = edge(a, b),
1090       bc = edge(b, c),
1091       ca = edge(c, a);
1092
1093   // sort edges by y-length
1094   if (ab.dy > bc.dy) { var t = ab; ab = bc; bc = t; }
1095   if (ab.dy > ca.dy) { var t = ab; ab = ca; ca = t; }
1096   if (bc.dy > ca.dy) { var t = bc; bc = ca; ca = t; }
1097
1098   // scan span! scan span!
1099   if (ab.dy) scanSpans(ca, ab, ymin, ymax, scanLine);
1100   if (bc.dy) scanSpans(ca, bc, ymin, ymax, scanLine);
1101 }
1102 po.image = function() {
1103   var image = po.layer(load, unload),
1104       url;
1105
1106   function load(tile) {
1107     var element = tile.element = po.svg("image"), size = image.map().tileSize();
1108     element.setAttribute("preserveAspectRatio", "none");
1109     element.setAttribute("width", size.x);
1110     element.setAttribute("height", size.y);
1111
1112     if (typeof url == "function") {
1113       element.setAttribute("opacity", 0);
1114       var tileUrl = url(tile);
1115       if (tileUrl != null) {
1116         tile.request = po.queue.image(element, tileUrl, function(img) {
1117           delete tile.request;
1118           tile.ready = true;
1119           tile.img = img;
1120           element.removeAttribute("opacity");
1121           image.dispatch({type: "load", tile: tile});
1122         });
1123       } else {
1124         tile.ready = true;
1125         image.dispatch({type: "load", tile: tile});
1126       }
1127     } else {
1128       tile.ready = true;
1129       if (url != null) element.setAttributeNS(po.ns.xlink, "href", url);
1130       image.dispatch({type: "load", tile: tile});
1131     }
1132   }
1133
1134   function unload(tile) {
1135     if (tile.request) tile.request.abort(true);
1136   }
1137
1138   image.url = function(x) {
1139     if (!arguments.length) return url;
1140     url = typeof x == "string" && /{.}/.test(x) ? po.url(x) : x;
1141     return image.reload();
1142   };
1143
1144   return image;
1145 };
1146 po.geoJson = function(fetch) {
1147   var geoJson = po.layer(load, unload),
1148       container = geoJson.container(),
1149       url,
1150       clip = true,
1151       clipId = "org.polymaps." + po.id(),
1152       clipHref = "url(#" + clipId + ")",
1153       clipPath = container.insertBefore(po.svg("clipPath"), container.firstChild),
1154       clipRect = clipPath.appendChild(po.svg("rect")),
1155       scale = "auto",
1156       zoom = null,
1157       features;
1158
1159   container.setAttribute("fill-rule", "evenodd");
1160   clipPath.setAttribute("id", clipId);
1161
1162   if (!arguments.length) fetch = po.queue.json;
1163
1164   function projection(proj) {
1165     var l = {lat: 0, lon: 0};
1166     return function(coordinates) {
1167       l.lat = coordinates[1];
1168       l.lon = coordinates[0];
1169       var p = proj(l);
1170       coordinates.x = p.x;
1171       coordinates.y = p.y;
1172       return p;
1173     };
1174   }
1175
1176   function geometry(o, proj) {
1177     return o && o.type in types && types[o.type](o, proj);
1178   }
1179
1180   var types = {
1181
1182     Point: function(o, proj) {
1183       var p = proj(o.coordinates),
1184           c = po.svg("circle");
1185       c.setAttribute("r", 4.5);
1186       c.setAttribute("transform", "translate(" + p.x + "," + p.y + ")");
1187       return c;
1188     },
1189
1190     MultiPoint: function(o, proj) {
1191       var g = po.svg("g"),
1192           c = o.coordinates,
1193           p, // proj(c[i])
1194           x, // svg:circle
1195           i = -1,
1196           n = c.length;
1197       while (++i < n) {
1198         x = g.appendChild(po.svg("circle"));
1199         x.setAttribute("r", 4.5);
1200         x.setAttribute("transform", "translate(" + (p = proj(c[i])).x + "," + p.y + ")");
1201       }
1202       return g;
1203     },
1204
1205     LineString: function(o, proj) {
1206       var x = po.svg("path"),
1207           d = ["M"],
1208           c = o.coordinates,
1209           p, // proj(c[i])
1210           i = -1,
1211           n = c.length;
1212       while (++i < n) d.push((p = proj(c[i])).x, ",", p.y, "L");
1213       d.pop();
1214       if (!d.length) return;
1215       x.setAttribute("d", d.join(""));
1216       return x;
1217     },
1218
1219     MultiLineString: function(o, proj) {
1220       var x = po.svg("path"),
1221           d = [],
1222           ci = o.coordinates,
1223           cj, // ci[i]
1224           i = -1,
1225           j,
1226           n = ci.length,
1227           m;
1228       while (++i < n) {
1229         cj = ci[i];
1230         j = -1;
1231         m = cj.length;
1232         d.push("M");
1233         while (++j < m) d.push((p = proj(cj[j])).x, ",", p.y, "L");
1234         d.pop();
1235       }
1236       if (!d.length) return;
1237       x.setAttribute("d", d.join(""));
1238       return x;
1239     },
1240
1241     Polygon: function(o, proj) {
1242       var x = po.svg("path"),
1243           d = [],
1244           ci = o.coordinates,
1245           cj, // ci[i]
1246           i = -1,
1247           j,
1248           n = ci.length,
1249           m;
1250       while (++i < n) {
1251         cj = ci[i];
1252         j = -1;
1253         m = cj.length - 1;
1254         d.push("M");
1255         while (++j < m) d.push((p = proj(cj[j])).x, ",", p.y, "L");
1256         d[d.length - 1] = "Z";
1257       }
1258       if (!d.length) return;
1259       x.setAttribute("d", d.join(""));
1260       return x;
1261     },
1262
1263     MultiPolygon: function(o, proj) {
1264       var x = po.svg("path"),
1265           d = [],
1266           ci = o.coordinates,
1267           cj, // ci[i]
1268           ck, // cj[j]
1269           i = -1,
1270           j,
1271           k,
1272           n = ci.length,
1273           m,
1274           l;
1275       while (++i < n) {
1276         cj = ci[i];
1277         j = -1;
1278         m = cj.length;
1279         while (++j < m) {
1280           ck = cj[j];
1281           k = -1;
1282           l = ck.length - 1;
1283           d.push("M");
1284           while (++k < l) d.push((p = proj(ck[k])).x, ",", p.y, "L");
1285           d[d.length - 1] = "Z";
1286         }
1287       }
1288       if (!d.length) return;
1289       x.setAttribute("d", d.join(""));
1290       return x;
1291     },
1292
1293     GeometryCollection: function(o, proj) {
1294       var g = po.svg("g"),
1295           i = -1,
1296           c = o.geometries,
1297           n = c.length,
1298           x;
1299       while (++i < n) {
1300         x = geometry(c[i], proj);
1301         if (x) g.appendChild(x);
1302       }
1303       return g;
1304     }
1305
1306   };
1307
1308   function rescale(o, e, k) {
1309     return o.type in rescales && rescales[o.type](o, e, k);
1310   }
1311
1312   var rescales = {
1313
1314     Point: function (o, e, k) {
1315       var p = o.coordinates;
1316       e.setAttribute("transform", "translate(" + p.x + "," + p.y + ")" + k);
1317     },
1318
1319     MultiPoint: function (o, e, k) {
1320       var c = o.coordinates,
1321           i = -1,
1322           n = p.length,
1323           x = e.firstChild,
1324           p;
1325       while (++i < n) {
1326         p = c[i];
1327         x.setAttribute("transform", "translate(" + p.x + "," + p.y + ")" + k);
1328         x = x.nextSibling;
1329       }
1330     }
1331
1332   };
1333
1334   function load(tile, proj) {
1335     var g = tile.element = po.svg("g");
1336     tile.features = [];
1337
1338     proj = projection(proj(tile).locationPoint);
1339
1340     function update(data) {
1341       var updated = [];
1342
1343       /* Fetch the next batch of features, if so directed. */
1344       if (data.next) tile.request = fetch(data.next.href, update);
1345
1346       /* Convert the GeoJSON to SVG. */
1347       switch (data.type) {
1348         case "FeatureCollection": {
1349           for (var i = 0; i < data.features.length; i++) {
1350             var feature = data.features[i],
1351                 element = geometry(feature.geometry, proj);
1352             if (element) updated.push({element: g.appendChild(element), data: feature});
1353           }
1354           break;
1355         }
1356         case "Feature": {
1357           var element = geometry(data.geometry, proj);
1358           if (element) updated.push({element: g.appendChild(element), data: data});
1359           break;
1360         }
1361         default: {
1362           var element = geometry(data, proj);
1363           if (element) updated.push({element: g.appendChild(element), data: {type: "Feature", geometry: data}});
1364           break;
1365         }
1366       }
1367
1368       tile.ready = true;
1369       updated.push.apply(tile.features, updated);
1370       geoJson.dispatch({type: "load", tile: tile, features: updated});
1371     }
1372
1373     if (url != null) {
1374       tile.request = fetch(typeof url == "function" ? url(tile) : url, update);
1375     } else {
1376       update({type: "FeatureCollection", features: features || []});
1377     }
1378   }
1379
1380   function unload(tile) {
1381     if (tile.request) tile.request.abort(true);
1382   }
1383
1384   function move() {
1385     var zoom = geoJson.map().zoom(),
1386         tiles = geoJson.cache.locks(), // visible tiles
1387         key, // key in locks
1388         tile, // locks[key]
1389         features, // tile.features
1390         i, // current feature index
1391         n, // current feature count, features.length
1392         feature, // features[i]
1393         k; // scale transform
1394     if (scale == "fixed") {
1395       for (key in tiles) {
1396         if ((tile = tiles[key]).scale != zoom) {
1397           k = "scale(" + Math.pow(2, tile.zoom - zoom) + ")";
1398           i = -1;
1399           n = (features = tile.features).length;
1400           while (++i < n) rescale((feature = features[i]).data.geometry, feature.element, k);
1401           tile.scale = zoom;
1402         }
1403       }
1404     } else {
1405       for (key in tiles) {
1406         i = -1;
1407         n = (features = (tile = tiles[key]).features).length;
1408         while (++i < n) rescale((feature = features[i]).data.geometry, feature.element, "");
1409         delete tile.scale;
1410       }
1411     }
1412   }
1413
1414   geoJson.url = function(x) {
1415     if (!arguments.length) return url;
1416     url = typeof x == "string" && /{.}/.test(x) ? po.url(x) : x;
1417     if (url != null) features = null;
1418     if (typeof url == "string") geoJson.tile(false);
1419     return geoJson.reload();
1420   };
1421
1422   geoJson.features = function(x) {
1423     if (!arguments.length) return features;
1424     if (features = x) {
1425       url = null;
1426       geoJson.tile(false);
1427     }
1428     return geoJson.reload();
1429   };
1430
1431   geoJson.clip = function(x) {
1432     if (!arguments.length) return clip;
1433     if (clip) container.removeChild(clipPath);
1434     if (clip = x) container.insertBefore(clipPath, container.firstChild);
1435     var locks = geoJson.cache.locks();
1436     for (var key in locks) {
1437       if (clip) locks[key].element.setAttribute("clip-path", clipHref);
1438       else locks[key].element.removeAttribute("clip-path");
1439     }
1440     return geoJson;
1441   };
1442
1443   var __tile__ = geoJson.tile;
1444   geoJson.tile = function(x) {
1445     if (arguments.length && !x) geoJson.clip(x);
1446     return __tile__.apply(geoJson, arguments);
1447   };
1448
1449   var __map__ = geoJson.map;
1450   geoJson.map = function(x) {
1451     if (x && clipRect) {
1452       var size = x.tileSize();
1453       clipRect.setAttribute("width", size.x);
1454       clipRect.setAttribute("height", size.y);
1455     }
1456     return __map__.apply(geoJson, arguments);
1457   };
1458
1459   geoJson.scale = function(x) {
1460     if (!arguments.length) return scale;
1461     if (scale = x) geoJson.on("move", move);
1462     else geoJson.off("move", move);
1463     if (geoJson.map()) move();
1464     return geoJson;
1465   };
1466
1467   geoJson.show = function(tile) {
1468     if (clip) tile.element.setAttribute("clip-path", clipHref);
1469     else tile.element.removeAttribute("clip-path");
1470     geoJson.dispatch({type: "show", tile: tile, features: tile.features});
1471     return geoJson;
1472   };
1473
1474   geoJson.reshow = function() {
1475     var locks = geoJson.cache.locks();
1476     for (var key in locks) geoJson.show(locks[key]);
1477     return geoJson;
1478   };
1479
1480   return geoJson;
1481 };
1482 po.dblclick = function() {
1483   var dblclick = {},
1484       zoom = "mouse",
1485       map,
1486       container;
1487
1488   function handle(e) {
1489     var z = map.zoom();
1490     if (e.shiftKey) z = Math.ceil(z) - z - 1;
1491     else z = 1 - z + Math.floor(z);
1492     zoom === "mouse" ? map.zoomBy(z, map.mouse(e)) : map.zoomBy(z);
1493   }
1494
1495   dblclick.zoom = function(x) {
1496     if (!arguments.length) return zoom;
1497     zoom = x;
1498     return dblclick;
1499   };
1500
1501   dblclick.map = function(x) {
1502     if (!arguments.length) return map;
1503     if (map) {
1504       container.removeEventListener("dblclick", handle, false);
1505       container = null;
1506     }
1507     if (map = x) {
1508       container = map.container();
1509       container.addEventListener("dblclick", handle, false);
1510     }
1511     return dblclick;
1512   };
1513
1514   return dblclick;
1515 };
1516 po.drag = function() {
1517   var drag = {},
1518       map,
1519       container,
1520       dragging;
1521
1522   function mousedown(e) {
1523     if (e.shiftKey) return;
1524     dragging = {
1525       x: e.clientX,
1526       y: e.clientY
1527     };
1528     map.focusableParent().focus();
1529     e.preventDefault();
1530     document.body.style.setProperty("cursor", "move", null);
1531   }
1532
1533   function mousemove(e) {
1534     if (!dragging) return;
1535     map.panBy({x: e.clientX - dragging.x, y: e.clientY - dragging.y});
1536     dragging.x = e.clientX;
1537     dragging.y = e.clientY;
1538   }
1539
1540   function mouseup(e) {
1541     if (!dragging) return;
1542     mousemove(e);
1543     dragging = null;
1544     document.body.style.removeProperty("cursor");
1545   }
1546
1547   drag.map = function(x) {
1548     if (!arguments.length) return map;
1549     if (map) {
1550       container.removeEventListener("mousedown", mousedown, false);
1551       container = null;
1552     }
1553     if (map = x) {
1554       container = map.container();
1555       container.addEventListener("mousedown", mousedown, false);
1556     }
1557     return drag;
1558   };
1559
1560   window.addEventListener("mousemove", mousemove, false);
1561   window.addEventListener("mouseup", mouseup, false);
1562
1563   return drag;
1564 };
1565 po.wheel = function() {
1566   var wheel = {},
1567       timePrev = 0,
1568       last = 0,
1569       smooth = true,
1570       zoom = "mouse",
1571       location,
1572       map,
1573       container;
1574
1575   function move(e) {
1576     location = null;
1577   }
1578
1579   // mousewheel events are totally broken!
1580   // https://bugs.webkit.org/show_bug.cgi?id=40441
1581   // not only that, but Chrome and Safari differ in re. to acceleration!
1582   var inner = document.createElement("div"),
1583       outer = document.createElement("div");
1584   outer.style.visibility = "hidden";
1585   outer.style.top = "0px";
1586   outer.style.height = "0px";
1587   outer.style.width = "0px";
1588   outer.style.overflowY = "scroll";
1589   inner.style.height = "2000px";
1590   outer.appendChild(inner);
1591   document.body.appendChild(outer);
1592
1593   function mousewheel(e) {
1594     var delta = e.wheelDelta || -e.detail,
1595         point;
1596
1597     /* Detect the pixels that would be scrolled by this wheel event. */
1598     if (delta) {
1599       if (smooth) {
1600         try {
1601           outer.scrollTop = 1000;
1602           outer.dispatchEvent(e);
1603           delta = 1000 - outer.scrollTop;
1604         } catch (error) {
1605           // Derp! Hope for the best?
1606         }
1607         delta *= .005;
1608       }
1609
1610       /* If smooth zooming is disabled, batch events into unit steps. */
1611       else {
1612         var timeNow = Date.now();
1613         if (timeNow - timePrev > 200) {
1614           delta = delta > 0 ? +1 : -1;
1615           timePrev = timeNow;
1616         } else {
1617           delta = 0;
1618         }
1619       }
1620     }
1621
1622     if (delta) {
1623       switch (zoom) {
1624         case "mouse": {
1625           point = map.mouse(e);
1626           if (!location) location = map.pointLocation(point);
1627           map.off("move", move).zoomBy(delta, point, location).on("move", move);
1628           break;
1629         }
1630         case "location": {
1631           map.zoomBy(delta, map.locationPoint(location), location);
1632           break;
1633         }
1634         default: { // center
1635           map.zoomBy(delta);
1636           break;
1637         }
1638       }
1639     }
1640
1641     e.preventDefault();
1642     return false; // for Firefox
1643   }
1644
1645   wheel.smooth = function(x) {
1646     if (!arguments.length) return smooth;
1647     smooth = x;
1648     return wheel;
1649   };
1650
1651   wheel.zoom = function(x, l) {
1652     if (!arguments.length) return zoom;
1653     zoom = x;
1654     location = l;
1655     if (map) {
1656       if (zoom == "mouse") map.on("move", move);
1657       else map.off("move", move);
1658     }
1659     return wheel;
1660   };
1661
1662   wheel.map = function(x) {
1663     if (!arguments.length) return map;
1664     if (map) {
1665       container.removeEventListener("mousemove", move, false);
1666       container.removeEventListener("mousewheel", mousewheel, false);
1667       container.removeEventListener("MozMousePixelScroll", mousewheel, false);
1668       container = null;
1669       map.off("move", move);
1670     }
1671     if (map = x) {
1672       if (zoom == "mouse") map.on("move", move);
1673       container = map.container();
1674       container.addEventListener("mousemove", move, false);
1675       container.addEventListener("mousewheel", mousewheel, false);
1676       container.addEventListener("MozMousePixelScroll", mousewheel, false);
1677     }
1678     return wheel;
1679   };
1680
1681   return wheel;
1682 };
1683 po.arrow = function() {
1684   var arrow = {},
1685       key = {left: 0, right: 0, up: 0, down: 0},
1686       last = 0,
1687       repeatTimer,
1688       repeatDelay = 250,
1689       repeatInterval = 50,
1690       speed = 16,
1691       map,
1692       parent;
1693
1694   function keydown(e) {
1695     if (e.ctrlKey || e.altKey || e.metaKey) return;
1696     var now = Date.now(), dx = 0, dy = 0;
1697     switch (e.keyCode) {
1698       case 37: {
1699         if (!key.left) {
1700           last = now;
1701           key.left = 1;
1702           if (!key.right) dx = speed;
1703         }
1704         break;
1705       }
1706       case 39: {
1707         if (!key.right) {
1708           last = now;
1709           key.right = 1;
1710           if (!key.left) dx = -speed;
1711         }
1712         break;
1713       }
1714       case 38: {
1715         if (!key.up) {
1716           last = now;
1717           key.up = 1;
1718           if (!key.down) dy = speed;
1719         }
1720         break;
1721       }
1722       case 40: {
1723         if (!key.down) {
1724           last = now;
1725           key.down = 1;
1726           if (!key.up) dy = -speed;
1727         }
1728         break;
1729       }
1730       default: return;
1731     }
1732     if (dx || dy) map.panBy({x: dx, y: dy});
1733     if (!repeatTimer && (key.left | key.right | key.up | key.down)) {
1734       repeatTimer = setInterval(repeat, repeatInterval);
1735     }
1736     e.preventDefault();
1737   }
1738
1739   function keyup(e) {
1740     last = Date.now();
1741     switch (e.keyCode) {
1742       case 37: key.left = 0; break;
1743       case 39: key.right = 0; break;
1744       case 38: key.up = 0; break;
1745       case 40: key.down = 0; break;
1746       default: return;
1747     }
1748     if (repeatTimer && !(key.left | key.right | key.up | key.down)) {
1749       repeatTimer = clearInterval(repeatTimer);
1750     }
1751     e.preventDefault();
1752   }
1753
1754   function keypress(e) {
1755     switch (e.charCode) {
1756       case 45: case 95: map.zoom(Math.ceil(map.zoom()) - 1); break; // - _
1757       case 43: case 61: map.zoom(Math.floor(map.zoom()) + 1); break; // = +
1758       default: return;
1759     }
1760     e.preventDefault();
1761   }
1762
1763   function repeat() {
1764     if (!map) return;
1765     if (Date.now() < last + repeatDelay) return;
1766     var dx = (key.left - key.right) * speed,
1767         dy = (key.up - key.down) * speed;
1768     if (dx || dy) map.panBy({x: dx, y: dy});
1769   }
1770
1771   arrow.map = function(x) {
1772     if (!arguments.length) return map;
1773     if (map) {
1774       parent.removeEventListener("keypress", keypress, false);
1775       parent.removeEventListener("keydown", keydown, false);
1776       parent.removeEventListener("keyup", keyup, false);
1777       parent = null;
1778     }
1779     if (map = x) {
1780       parent = map.focusableParent();
1781       parent.addEventListener("keypress", keypress, false);
1782       parent.addEventListener("keydown", keydown, false);
1783       parent.addEventListener("keyup", keyup, false);
1784     }
1785     return arrow;
1786   };
1787
1788   arrow.speed = function(x) {
1789     if (!arguments.length) return speed;
1790     speed = x;
1791     return arrow;
1792   };
1793
1794   return arrow;
1795 };
1796 po.hash = function() {
1797   var hash = {},
1798       s0, // cached location.hash
1799       lat = 90 - 1e-8, // allowable latitude range
1800       map;
1801
1802   var parser = function(map, s) {
1803     var args = s.split("/").map(Number);
1804     if (args.length < 3 || args.some(isNaN)) return true; // replace bogus hash
1805     else {
1806       var size = map.size();
1807       map.zoomBy(args[0] - map.zoom(),
1808           {x: size.x / 2, y: size.y / 2},
1809           {lat: Math.min(lat, Math.max(-lat, args[1])), lon: args[2]});
1810     }
1811   };
1812
1813   var formatter = function(map) {
1814     var center = map.center(),
1815         zoom = map.zoom(),
1816         precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
1817     return "#" + zoom.toFixed(2)
1818              + "/" + center.lat.toFixed(precision)
1819              + "/" + center.lon.toFixed(precision);
1820   };
1821
1822   function move() {
1823     var s1 = formatter(map);
1824     if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map!
1825   }
1826
1827   function hashchange() {
1828     if (location.hash === s0) return; // ignore spurious hashchange events
1829     if (parser(map, (s0 = location.hash).substring(1)))
1830       move(); // replace bogus hash
1831   }
1832
1833   hash.map = function(x) {
1834     if (!arguments.length) return map;
1835     if (map) {
1836       map.off("move", move);
1837       window.removeEventListener("hashchange", hashchange, false);
1838     }
1839     if (map = x) {
1840       map.on("move", move);
1841       window.addEventListener("hashchange", hashchange, false);
1842       location.hash ? hashchange() : move();
1843     }
1844     return hash;
1845   };
1846
1847   hash.parser = function(x) {
1848     if (!arguments.length) return parser;
1849     parser = x;
1850     return hash;
1851   };
1852
1853   hash.formatter = function(x) {
1854     if (!arguments.length) return formatter;
1855     formatter = x;
1856     return hash;
1857   };
1858
1859   return hash;
1860 };
1861 po.touch = function() {
1862   var touch = {},
1863       map,
1864       container,
1865       rotate = false,
1866       last = 0,
1867       zoom,
1868       angle,
1869       locations = {}; // touch identifier -> location
1870
1871   window.addEventListener("touchmove", touchmove, false);
1872
1873   function touchstart(e) {
1874     var i = -1,
1875         n = e.touches.length,
1876         t = Date.now();
1877
1878     // doubletap detection
1879     if ((n == 1) && (t - last < 300)) {
1880       var z = map.zoom();
1881       map.zoomBy(1 - z + Math.floor(z), map.mouse(e.touches[0]));
1882       e.preventDefault();
1883     }
1884     last = t;
1885
1886     // store original zoom & touch locations
1887     zoom = map.zoom();
1888     angle = map.angle();
1889     while (++i < n) {
1890       t = e.touches[i];
1891       locations[t.identifier] = map.pointLocation(map.mouse(t));
1892     }
1893   }
1894
1895   function touchmove(e) {
1896     switch (e.touches.length) {
1897       case 1: { // single-touch pan
1898         var t0 = e.touches[0];
1899         map.zoomBy(0, map.mouse(t0), locations[t0.identifier]);
1900         e.preventDefault();
1901         break;
1902       }
1903       case 2: { // double-touch pan + zoom + rotate
1904         var t0 = e.touches[0],
1905             t1 = e.touches[1],
1906             p0 = map.mouse(t0),
1907             p1 = map.mouse(t1),
1908             p2 = {x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2}, // center point
1909             c0 = po.map.locationCoordinate(locations[t0.identifier]),
1910             c1 = po.map.locationCoordinate(locations[t1.identifier]),
1911             c2 = {row: (c0.row + c1.row) / 2, column: (c0.column + c1.column) / 2, zoom: 0},
1912             l2 = po.map.coordinateLocation(c2); // center location
1913         map.zoomBy(Math.log(e.scale) / Math.LN2 + zoom - map.zoom(), p2, l2);
1914         if (rotate) map.angle(e.rotation / 180 * Math.PI + angle);
1915         e.preventDefault();
1916         break;
1917       }
1918     }
1919   }
1920
1921   touch.rotate = function(x) {
1922     if (!arguments.length) return rotate;
1923     rotate = x;
1924     return touch;
1925   };
1926
1927   touch.map = function(x) {
1928     if (!arguments.length) return map;
1929     if (map) {
1930       container.removeEventListener("touchstart", touchstart, false);
1931       container = null;
1932     }
1933     if (map = x) {
1934       container = map.container();
1935       container.addEventListener("touchstart", touchstart, false);
1936     }
1937     return touch;
1938   };
1939
1940   return touch;
1941 };
1942 // Default map controls.
1943 po.interact = function() {
1944   var interact = {},
1945       drag = po.drag(),
1946       wheel = po.wheel(),
1947       dblclick = po.dblclick(),
1948       touch = po.touch(),
1949       arrow = po.arrow();
1950
1951   interact.map = function(x) {
1952     drag.map(x);
1953     wheel.map(x);
1954     dblclick.map(x);
1955     touch.map(x);
1956     arrow.map(x);
1957     return interact;
1958   };
1959
1960   return interact;
1961 };
1962 po.compass = function() {
1963   var compass = {},
1964       g = po.svg("g"),
1965       ticks = {},
1966       r = 30,
1967       speed = 16,
1968       last = 0,
1969       repeatDelay = 250,
1970       repeatInterval = 50,
1971       position = "top-left", // top-left, top-right, bottom-left, bottom-right
1972       zoomStyle = "small", // none, small, big
1973       zoomContainer,
1974       panStyle = "small", // none, small
1975       panTimer,
1976       panDirection,
1977       panContainer,
1978       drag,
1979       dragRect = po.svg("rect"),
1980       map,
1981       container,
1982       window;
1983
1984   g.setAttribute("class", "compass");
1985   dragRect.setAttribute("class", "back fore");
1986   dragRect.setAttribute("pointer-events", "none");
1987   dragRect.setAttribute("display", "none");
1988
1989   function panStart(e) {
1990     g.setAttribute("class", "compass active");
1991     if (!panTimer) panTimer = setInterval(panRepeat, repeatInterval);
1992     if (panDirection) map.panBy(panDirection);
1993     last = Date.now();
1994     return cancel(e);
1995   }
1996
1997   function panRepeat() {
1998     if (panDirection && (Date.now() > last + repeatDelay)) {
1999       map.panBy(panDirection);
2000     }
2001   }
2002
2003   function mousedown(e) {
2004     if (e.shiftKey) {
2005       drag = {x0: map.mouse(e)};
2006       map.focusableParent().focus();
2007       return cancel(e);
2008     }
2009   }
2010
2011   function mousemove(e) {
2012     if (!drag) return;
2013     drag.x1 = map.mouse(e);
2014     dragRect.setAttribute("x", Math.min(drag.x0.x, drag.x1.x));
2015     dragRect.setAttribute("y", Math.min(drag.x0.y, drag.x1.y));
2016     dragRect.setAttribute("width", Math.abs(drag.x0.x - drag.x1.x));
2017     dragRect.setAttribute("height", Math.abs(drag.x0.y - drag.x1.y));
2018     dragRect.removeAttribute("display");
2019   }
2020
2021   function mouseup(e) {
2022     g.setAttribute("class", "compass");
2023     if (drag) {
2024       if (drag.x1) {
2025         map.extent([
2026           map.pointLocation({
2027             x: Math.min(drag.x0.x, drag.x1.x),
2028             y: Math.max(drag.x0.y, drag.x1.y)
2029           }),
2030           map.pointLocation({
2031             x: Math.max(drag.x0.x, drag.x1.x),
2032             y: Math.min(drag.x0.y, drag.x1.y)
2033           })
2034         ]);
2035         dragRect.setAttribute("display", "none");
2036       }
2037       drag = null;
2038     }
2039     if (panTimer) {
2040       clearInterval(panTimer);
2041       panTimer = 0;
2042     }
2043   }
2044
2045   function panBy(x) {
2046     return function() {
2047       x ? this.setAttribute("class", "active") : this.removeAttribute("class");
2048       panDirection = x;
2049     };
2050   }
2051
2052   function zoomBy(x) {
2053     return function(e) {
2054       g.setAttribute("class", "compass active");
2055       var z = map.zoom();
2056       map.zoom(x < 0 ? Math.ceil(z) - 1 : Math.floor(z) + 1);
2057       return cancel(e);
2058     };
2059   }
2060
2061   function zoomTo(x) {
2062     return function(e) {
2063       map.zoom(x);
2064       return cancel(e);
2065     };
2066   }
2067
2068   function zoomOver() {
2069     this.setAttribute("class", "active");
2070   }
2071
2072   function zoomOut() {
2073     this.removeAttribute("class");
2074   }
2075
2076   function cancel(e) {
2077     e.stopPropagation();
2078     e.preventDefault();
2079     return false;
2080   }
2081
2082   function pan(by) {
2083     var x = Math.SQRT1_2 * r,
2084         y = r * .7,
2085         z = r * .2,
2086         g = po.svg("g"),
2087         dir = g.appendChild(po.svg("path")),
2088         chv = g.appendChild(po.svg("path"));
2089     dir.setAttribute("class", "direction");
2090     dir.setAttribute("pointer-events", "all");
2091     dir.setAttribute("d", "M0,0L" + x + "," + x + "A" + r + "," + r + " 0 0,1 " + -x + "," + x + "Z");
2092     chv.setAttribute("class", "chevron");
2093     chv.setAttribute("d", "M" + z + "," + (y - z) + "L0," + y + " " + -z + "," + (y - z));
2094     chv.setAttribute("pointer-events", "none");
2095     g.addEventListener("mousedown", panStart, false);
2096     g.addEventListener("mouseover", panBy(by), false);
2097     g.addEventListener("mouseout", panBy(null), false);
2098     g.addEventListener("dblclick", cancel, false);
2099     return g;
2100   }
2101
2102   function zoom(by) {
2103     var x = r * .4,
2104         y = x / 2,
2105         g = po.svg("g"),
2106         back = g.appendChild(po.svg("path")),
2107         dire = g.appendChild(po.svg("path")),
2108         chev = g.appendChild(po.svg("path")),
2109         fore = g.appendChild(po.svg("path"));
2110     back.setAttribute("class", "back");
2111     back.setAttribute("d", "M" + -x + ",0V" + -x + "A" + x + "," + x + " 0 1,1 " + x + "," + -x + "V0Z");
2112     dire.setAttribute("class", "direction");
2113     dire.setAttribute("d", back.getAttribute("d"));
2114     chev.setAttribute("class", "chevron");
2115     chev.setAttribute("d", "M" + -y + "," + -x + "H" + y + (by > 0 ? "M0," + (-x - y) + "V" + -y : ""));
2116     fore.setAttribute("class", "fore");
2117     fore.setAttribute("fill", "none");
2118     fore.setAttribute("d", back.getAttribute("d"));
2119     g.addEventListener("mousedown", zoomBy(by), false);
2120     g.addEventListener("mouseover", zoomOver, false);
2121     g.addEventListener("mouseout", zoomOut, false);
2122     g.addEventListener("dblclick", cancel, false);
2123     return g;
2124   }
2125
2126   function tick(i) {
2127     var x = r * .2,
2128         y = r * .4,
2129         g = po.svg("g"),
2130         back = g.appendChild(po.svg("rect")),
2131         chev = g.appendChild(po.svg("path"));
2132     back.setAttribute("pointer-events", "all");
2133     back.setAttribute("fill", "none");
2134     back.setAttribute("x", -y);
2135     back.setAttribute("y", -.75 * y);
2136     back.setAttribute("width", 2 * y);
2137     back.setAttribute("height", 1.5 * y);
2138     chev.setAttribute("class", "chevron");
2139     chev.setAttribute("d", "M" + -x + ",0H" + x);
2140     g.addEventListener("mousedown", zoomTo(i), false);
2141     g.addEventListener("dblclick", cancel, false);
2142     return g;
2143   }
2144
2145   function move() {
2146     var x = r + 6, y = x, size = map.size();
2147     switch (position) {
2148       case "top-left": break;
2149       case "top-right": x = size.x - x; break;
2150       case "bottom-left": y = size.y - y; break;
2151       case "bottom-right": x = size.x - x; y = size.y - y; break;
2152     }
2153     g.setAttribute("transform", "translate(" + x + "," + y + ")");
2154     dragRect.setAttribute("transform", "translate(" + -x + "," + -y + ")");
2155     for (var i in ticks) {
2156       i == map.zoom()
2157           ? ticks[i].setAttribute("class", "active")
2158           : ticks[i].removeAttribute("class");
2159     }
2160   }
2161
2162   function draw() {
2163     while (g.lastChild) g.removeChild(g.lastChild);
2164
2165     g.appendChild(dragRect);
2166
2167     if (panStyle != "none") {
2168       panContainer = g.appendChild(po.svg("g"));
2169       panContainer.setAttribute("class", "pan");
2170
2171       var back = panContainer.appendChild(po.svg("circle"));
2172       back.setAttribute("class", "back");
2173       back.setAttribute("r", r);
2174
2175       var s = panContainer.appendChild(pan({x: 0, y: -speed}));
2176       s.setAttribute("transform", "rotate(0)");
2177
2178       var w = panContainer.appendChild(pan({x: speed, y: 0}));
2179       w.setAttribute("transform", "rotate(90)");
2180
2181       var n = panContainer.appendChild(pan({x: 0, y: speed}));
2182       n.setAttribute("transform", "rotate(180)");
2183
2184       var e = panContainer.appendChild(pan({x: -speed, y: 0}));
2185       e.setAttribute("transform", "rotate(270)");
2186
2187       var fore = panContainer.appendChild(po.svg("circle"));
2188       fore.setAttribute("fill", "none");
2189       fore.setAttribute("class", "fore");
2190       fore.setAttribute("r", r);
2191     } else {
2192       panContainer = null;
2193     }
2194
2195     if (zoomStyle != "none") {
2196       zoomContainer = g.appendChild(po.svg("g"));
2197       zoomContainer.setAttribute("class", "zoom");
2198
2199       var j = -.5;
2200       if (zoomStyle == "big") {
2201         ticks = {};
2202         for (var i = map.zoomRange()[0], j = 0; i <= map.zoomRange()[1]; i++, j++) {
2203           (ticks[i] = zoomContainer.appendChild(tick(i)))
2204               .setAttribute("transform", "translate(0," + (-(j + .75) * r * .4) + ")");
2205         }
2206       }
2207
2208       var p = panStyle == "none" ? .4 : 2;
2209       zoomContainer.setAttribute("transform", "translate(0," + r * (/^top-/.test(position) ? (p + (j + .5) * .4) : -p) + ")");
2210       zoomContainer.appendChild(zoom(+1)).setAttribute("transform", "translate(0," + (-(j + .5) * r * .4) + ")");
2211       zoomContainer.appendChild(zoom(-1)).setAttribute("transform", "scale(-1)");
2212     } else {
2213       zoomContainer = null;
2214     }
2215
2216     move();
2217   }
2218
2219   compass.radius = function(x) {
2220     if (!arguments.length) return r;
2221     r = x;
2222     if (map) draw();
2223     return compass;
2224   };
2225
2226   compass.speed = function(x) {
2227     if (!arguments.length) return r;
2228     speed = x;
2229     return compass;
2230   };
2231
2232   compass.position = function(x) {
2233     if (!arguments.length) return position;
2234     position = x;
2235     if (map) draw();
2236     return compass;
2237   };
2238
2239   compass.pan = function(x) {
2240     if (!arguments.length) return panStyle;
2241     panStyle = x;
2242     if (map) draw();
2243     return compass;
2244   };
2245
2246   compass.zoom = function(x) {
2247     if (!arguments.length) return zoomStyle;
2248     zoomStyle = x;
2249     if (map) draw();
2250     return compass;
2251   };
2252
2253   compass.map = function(x) {
2254     if (!arguments.length) return map;
2255     if (map) {
2256       container.removeEventListener("mousedown", mousedown, false);
2257       container.removeChild(g);
2258       container = null;
2259       window.removeEventListener("mousemove", mousemove, false);
2260       window.removeEventListener("mouseup", mouseup, false);
2261       window = null;
2262       map.off("move", move).off("resize", move);
2263     }
2264     if (map = x) {
2265       container = map.container();
2266       container.appendChild(g);
2267       container.addEventListener("mousedown", mousedown, false);
2268       window = container.ownerDocument.defaultView;
2269       window.addEventListener("mousemove", mousemove, false);
2270       window.addEventListener("mouseup", mouseup, false);
2271       map.on("move", move).on("resize", move);
2272       draw();
2273     }
2274     return compass;
2275   };
2276
2277   return compass;
2278 };
2279 po.grid = function() {
2280   var grid = {},
2281       map,
2282       g = po.svg("g");
2283
2284   g.setAttribute("class", "grid");
2285
2286   function move(e) {
2287     var p,
2288         line = g.firstChild,
2289         size = map.size(),
2290         nw = map.pointLocation(zero),
2291         se = map.pointLocation(size),
2292         step = Math.pow(2, 4 - Math.round(map.zoom()));
2293
2294     // Round to step.
2295     nw.lat = Math.floor(nw.lat / step) * step;
2296     nw.lon = Math.ceil(nw.lon / step) * step;
2297
2298     // Longitude ticks.
2299     for (var x; (x = map.locationPoint(nw).x) <= size.x; nw.lon += step) {
2300       if (!line) line = g.appendChild(po.svg("line"));
2301       line.setAttribute("x1", x);
2302       line.setAttribute("x2", x);
2303       line.setAttribute("y1", 0);
2304       line.setAttribute("y2", size.y);
2305       line = line.nextSibling;
2306     }
2307
2308     // Latitude ticks.
2309     for (var y; (y = map.locationPoint(nw).y) <= size.y; nw.lat -= step) {
2310       if (!line) line = g.appendChild(po.svg("line"));
2311       line.setAttribute("y1", y);
2312       line.setAttribute("y2", y);
2313       line.setAttribute("x1", 0);
2314       line.setAttribute("x2", size.x);
2315       line = line.nextSibling;
2316     }
2317
2318     // Remove extra ticks.
2319     while (line) {
2320       var next = line.nextSibling;
2321       g.removeChild(line);
2322       line = next;
2323     }
2324   }
2325
2326   grid.map = function(x) {
2327     if (!arguments.length) return map;
2328     if (map) {
2329       g.parentNode.removeChild(g);
2330       map.off("move", move).off("resize", move);
2331     }
2332     if (map = x) {
2333       map.on("move", move).on("resize", move);
2334       map.container().appendChild(g);
2335       map.dispatch({type: "move"});
2336     }
2337     return grid;
2338   };
2339
2340   return grid;
2341 };
2342 po.stylist = function() {
2343   var attrs = [],
2344       styles = [],
2345       title;
2346
2347   function stylist(e) {
2348     var ne = e.features.length,
2349         na = attrs.length,
2350         ns = styles.length,
2351         f, // feature
2352         d, // data
2353         o, // element
2354         x, // attr or style or title descriptor
2355         v, // attr or style or title value
2356         i,
2357         j;
2358     for (i = 0; i < ne; ++i) {
2359       if (!(o = (f = e.features[i]).element)) continue;
2360       d = f.data;
2361       for (j = 0; j < na; ++j) {
2362         v = (x = attrs[j]).value;
2363         if (typeof v === "function") v = v.call(null, d);
2364         v == null ? (x.name.local
2365             ? o.removeAttributeNS(x.name.space, x.name.local)
2366             : o.removeAttribute(x.name)) : (x.name.local
2367             ? o.setAttributeNS(x.name.space, x.name.local, v)
2368             : o.setAttribute(x.name, v));
2369       }
2370       for (j = 0; j < ns; ++j) {
2371         v = (x = styles[j]).value;
2372         if (typeof v === "function") v = v.call(null, d);
2373         v == null
2374             ? o.style.removeProperty(x.name)
2375             : o.style.setProperty(x.name, v, x.priority);
2376       }
2377       if (v = title) {
2378         if (typeof v === "function") v = v.call(null, d);
2379         while (o.lastChild) o.removeChild(o.lastChild);
2380         if (v != null) o.appendChild(po.svg("title")).appendChild(document.createTextNode(v));
2381       }
2382     }
2383   }
2384
2385   stylist.attr = function(n, v) {
2386     attrs.push({name: ns(n), value: v});
2387     return stylist;
2388   };
2389
2390   stylist.style = function(n, v, p) {
2391     styles.push({name: n, value: v, priority: arguments.length < 3 ? null : p});
2392     return stylist;
2393   };
2394
2395   stylist.title = function(v) {
2396     title = v;
2397     return stylist;
2398   };
2399
2400   return stylist;
2401 };
2402 })(org.polymaps);