2.0_beta sync to rsa
[framework/web/web-ui-fw.git] / libs / js / jquery-geo-1.0a4 / js / jquery.geo.geomap.js
1 (function ($, undefined) {
2   var _ieVersion = (function () {
3     var v = 5, div = document.createElement("div"), a = div.all || [];
4     while (div.innerHTML = "<!--[if gt IE " + (++v) + "]><br><![endif]-->", a[0]) { }
5     return v > 6 ? v : !v;
6   } ()),
7
8       _defaultOptions = {
9         bbox: [-180, -85, 180, 85],
10         bboxMax: [-180, -85, 180, 85],
11         center: [0, 0],
12         cursors: {
13           "static": "default",
14           pan: "url(data:image/vnd.microsoft.icon;base64,AAACAAEAICACAAgACAAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAA/AAAAfwAAAP+AAAH/gAAB/8AAA//AAAd/wAAGf+AAAH9gAADbYAAA2yAAAZsAAAGbAAAAGAAAAAAAAA//////////////////////////////////////////////////////////////////////////////////////gH///4B///8Af//+AD///AA///wAH//4AB//8AAf//AAD//5AA///gAP//4AD//8AF///AB///5A////5///8=), move",
15           zoom: "crosshair",
16           drawPoint: "crosshair",
17           drawLineString: "crosshair",
18           drawPolygon: "crosshair",
19           measureLength: "crosshair",
20           measureArea: "crosshair"
21         },
22         measureLabels: {
23           length: "{{=length.toFixed( 2 )}} m",
24           area: "{{=area.toFixed( 2 )}} sq m"
25         },
26         drawStyle: {},
27         shapeStyle: {},
28         mode: "pan",
29         pannable: true,
30         scroll: "default",
31         services: [
32             {
33               "class": "osm",
34               type: "tiled",
35               src: function (view) {
36                 return "http://tile.openstreetmap.org/" + view.zoom + "/" + view.tile.column + "/" + view.tile.row + ".png";
37               },
38               attr: "&copy; OpenStreetMap &amp; contributors, CC-BY-SA"
39             }
40           ],
41         tilingScheme: {
42           tileWidth: 256,
43           tileHeight: 256,
44           levels: 18,
45           basePixelSize: 156543.03392799936,
46           origin: [-20037508.342787, 20037508.342787]
47         },
48         axisLayout: "map",
49         zoom: 0,
50         pixelSize: 0
51       };
52
53   $.widget("geo.geomap", {
54     // private widget members
55     _$elem: undefined, //< map div for maps, service div for services
56     _map: undefined, //< only defined in services
57     _created: false,
58
59     _contentBounds: {},
60
61     _$resizeContainer: undefined, //< all elements that should match _contentBounds' size
62
63     _$eventTarget: undefined,
64     _$contentFrame: undefined,
65     _$existingChildren: undefined,
66     _$attrList: undefined,
67     _$servicesContainer: undefined,
68
69     _$panContainer: undefined, //< all non-service elements that move while panning
70     _$shapesContainer: undefined,
71     _$drawContainer: undefined,
72     _$measureContainer: undefined,
73     _$measureLabel: undefined,
74
75     _dpi: 96,
76
77     _currentServices: [], //< internal copy
78
79     _center: undefined,
80     _pixelSize: undefined,
81     _centerMax: undefined,
82     _pixelSizeMax: undefined,
83
84     _userGeodetic: true,
85
86     _wheelTimeout: null,
87     _wheelLevel: 0,
88
89     _zoomFactor: 2, //< determines what a zoom level means
90
91     _fullZoomFactor: 2, //< interactiveScale factor needed to zoom a whole level
92     _partialZoomFactor: 1.18920711500273, //< interactiveScale factor needed to zoom a fraction of a level (the fourth root of 2)
93
94     _mouseDown: undefined,
95     _inOp: undefined,
96     _toolPan: undefined,
97     _shiftZoom: undefined,
98     _anchor: undefined,
99     _current: undefined,
100     _downDate: undefined,
101     _moveDate: undefined,
102     _clickDate: undefined,
103     _lastMove: undefined,
104     _lastDrag: undefined,
105
106     _windowHandler: null,
107     _resizeTimeout: null,
108
109     _panning: undefined,
110     _velocity: undefined,
111     _friction: undefined,
112
113     _supportTouch: undefined,
114     _softDblClick: undefined,
115     _isTap: undefined,
116     _isDbltap: undefined,
117
118     _isMultiTouch: undefined,
119     _multiTouchAnchor: undefined, //< TouchList
120     _multiTouchAnchorBbox: undefined, //< bbox
121     _multiTouchCurrentBbox: undefined, //< bbox
122
123     _drawTimeout: null, //< used in drawPoint mode so we don't send two shape events on dbltap
124     _drawPixels: [], //< an array of coordinate arrays for drawing lines & polygons, in pixel coordinates
125     _drawCoords: [],
126
127     _graphicShapes: [], //< an array of objects containing style object refs & GeoJSON object refs
128
129     _initOptions: {},
130
131     _options: {},
132
133     options: $.extend({}, _defaultOptions),
134
135     _createWidget: function (options, element) {
136       this._$elem = $(element);
137
138       if (this._$elem.is(".geo-service")) {
139         var $contentFrame = this._$elem.closest( ".geo-content-frame" );
140         this._$elem.append('<div class="geo-shapes-container" style="position:absolute; left:0; top:0; width:' + $contentFrame.css( "width" ) + '; height:' + $contentFrame.css( "height" ) + '; margin:0; padding:0;"></div>');
141         this._$shapesContainer = this._$elem.children(':last');
142         this._graphicShapes = [];
143         $.Widget.prototype._createWidget.apply(this, arguments);
144         return;
145       }
146
147       this._$elem.addClass("geo-map");
148
149       this._initOptions = options || {};
150
151       this._forcePosition(this._$elem);
152
153       this._$elem.css("text-align", "left");
154
155       var size = this._findMapSize();
156       this._contentBounds = {
157         x: parseInt(this._$elem.css("padding-left")),
158         y: parseInt(this._$elem.css("padding-top")),
159         width: size["width"],
160         height: size["height"]
161       };
162
163       this._createChildren();
164
165       this._center = this._centerMax = [0, 0];
166
167       this.options["pixelSize"] = this._pixelSize = this._pixelSizeMax = 156543.03392799936;
168
169       this._mouseDown =
170           this._inOp =
171           this._toolPan =
172           this._shiftZoom =
173           this._panning =
174           this._isTap =
175           this._isDbltap = false;
176
177       this._anchor = [ 0, 0 ];
178       this._current = [ 0, 0 ];
179       this._lastMove = [ 0, 0 ];
180       this._lastDrag = [ 0, 0 ];
181       this._velocity = [ 0, 0 ];
182
183       this._friction = [.8, .8];
184
185       this._downDate =
186           this._moveDate =
187           this._clickDate = 0;
188
189       this._drawPixels = [];
190       this._drawCoords =  [];
191       this._graphicShapes = [];
192
193
194       $.Widget.prototype._createWidget.apply(this, arguments);
195     },
196
197     _create: function () {
198       this._options = this.options;
199
200       if (this._$elem.is(".geo-service")) {
201         this._map = this._$elem.data( "geoMap" );
202         this._$shapesContainer.geographics( );
203         this._options["shapeStyle"] = this._$shapesContainer.geographics("option", "style");
204         return;
205       }
206
207       this._map = this;
208
209       this._supportTouch = "ontouchend" in document;
210       this._softDblClick = this._supportTouch || _ieVersion == 7;
211
212       var geomap = this,
213           touchStartEvent = this._supportTouch ? "touchstart" : "mousedown",
214           touchStopEvent = this._supportTouch ? "touchend touchcancel" : "mouseup",
215           touchMoveEvent = this._supportTouch ? "touchmove" : "mousemove";
216
217       $(document).keydown($.proxy(this._document_keydown, this));
218
219       this._$eventTarget.dblclick($.proxy(this._eventTarget_dblclick, this));
220
221       this._$eventTarget.bind(touchStartEvent, $.proxy(this._eventTarget_touchstart, this));
222
223       var dragTarget = (this._$eventTarget[0].setCapture) ? this._$eventTarget : $(document);
224       dragTarget.bind(touchMoveEvent, $.proxy(this._dragTarget_touchmove, this));
225       dragTarget.bind(touchStopEvent, $.proxy(this._dragTarget_touchstop, this));
226
227       this._$eventTarget.mousewheel($.proxy(this._eventTarget_mousewheel, this));
228
229       this._windowHandler = function () {
230         if (geomap._resizeTimeout) {
231           clearTimeout(geomap._resizeTimeout);
232         }
233         geomap._resizeTimeout = setTimeout(function () {
234           if (geomap._created) {
235             geomap._$elem.geomap("resize");
236           }
237         }, 500);
238       };
239
240       $(window).resize(this._windowHandler);
241
242       this._$drawContainer.geographics({ style: this._initOptions.drawStyle || {} });
243       this._options["drawStyle"] = this._$drawContainer.geographics("option", "style");
244
245       this._$shapesContainer.geographics( { style: this._initOptions.shapeStyle || { } } );
246       this._options["shapeStyle"] = this._$shapesContainer.geographics("option", "style");
247
248       if (this._initOptions) {
249         if (this._initOptions.tilingScheme) {
250           this._setOption("tilingScheme", this._initOptions.tilingScheme, false);
251         }
252         if ( this._initOptions.services ) {
253           // jQuery UI Widget Factory merges user services with our default, we want to clobber the default
254           this._options[ "services" ] = $.merge( [ ], this._initOptions.services );
255         }
256         if (this._initOptions.bbox) {
257           this._setOption("bbox", this._initOptions.bbox, false);
258         }
259         if (this._initOptions.center) {
260           this._setOption("center", this._initOptions.center, false);
261         }
262         if (this._initOptions.zoom !== undefined) {
263           this._setZoom(this._initOptions.zoom, false, false);
264         }
265       }
266
267       $.template( "geoMeasureLength", this._options[ "measureLabels" ].length );
268       $.template( "geoMeasureArea", this._options[ "measureLabels" ].area );
269
270       this._$eventTarget.css("cursor", this._options["cursors"][this._options["mode"]]);
271
272       this._createServices();
273       this._refresh();
274
275       this._created = true;
276     },
277
278     _setOption: function (key, value, refresh) {
279       if ( key == "pixelSize" ) {
280         return;
281       }
282
283       refresh = (refresh === undefined || refresh);
284
285       if ( this._$elem.is( ".geo-map" ) ) {
286         this._panFinalize();
287       }
288
289       switch (key) {
290         case "bbox":
291           this._userGeodetic = $.geo.proj && $.geo._isGeodetic( value );
292           if ( this._userGeodetic ) {
293             value = $.geo.proj.fromGeodetic( value );
294           }
295
296           this._setBbox(value, false, refresh);
297           value = this._getBbox();
298           break;
299
300         case "center":
301           this._userGeodetic = $.geo.proj && $.geo._isGeodetic( value );
302           if ( this._userGeodetic ) {
303             value = $.geo.proj.fromGeodetic( value );
304           }
305
306           this._setCenterAndSize( value, this._pixelSize, false, refresh );
307           break;
308
309         case "measureLabels":
310           value = $.extend( this._options[ "measureLabels" ], value );
311           $.template( "geoMeasureLength", value.length );
312           $.template( "geoMeasureArea", value.area );
313           break;
314
315         case "drawStyle":
316           if (this._$drawContainer) {
317             this._$drawContainer.geographics("option", "style", value);
318             value = this._$drawContainer.geographics("option", "style");
319           }
320           break;
321
322         case "shapeStyle":
323           if (this._$shapesContainer) {
324             this._$shapesContainer.geographics("option", "style", value);
325             value = this._$shapesContainer.geographics("option", "style");
326           }
327           break;
328
329         case "mode":
330           this._resetDrawing( );
331           this._$eventTarget.css("cursor", this._options["cursors"][value]);
332           break;
333
334         case "zoom":
335           this._setZoom(value, false, refresh);
336           break;
337       }
338
339       $.Widget.prototype._setOption.apply(this, arguments);
340
341       switch ( key ) {
342         case "bbox":
343         case "center":
344           if ( this._userGeodetic ) {
345             this._options[ "bbox" ] = $.geo.proj.toGeodetic( this._options[ "bbox" ] );
346             this._options[ "center" ] = $.geo.proj.toGeodetic( this._center );
347           }
348           break;
349
350         case "tilingScheme":
351           if ( value != null ) {
352             this._pixelSizeMax = this._getPixelSize( 0 );
353             this._centerMax = [
354               value.origin[ 0 ] + this._pixelSizeMax * value.tileWidth / 2,
355               value.origin[ 1 ] + this._pixelSizeMax * value.tileHeight / 2
356             ];
357           }
358           break;
359
360         case "bboxMax":
361           this._pixelSizeMax = this._getPixelSize( 0 );
362
363           if ( $.geo.proj && $.geo._isGeodetic( value ) ) {
364             this._centerMax = $.geo.center( $.geo.proj.fromGeodetic( value ) );
365           } else {
366             this._centerMax = $.geo.center( value );
367           }
368           break;
369
370         case "services":
371           this._createServices();
372           if (refresh) {
373             this._refresh();
374           }
375           break;
376
377         case "shapeStyle":
378           if ( refresh ) {
379             this._$shapesContainer.geographics("clear");
380             this._refreshShapes( this._$shapesContainer, this._graphicShapes, this._graphicShapes, this._graphicShapes );
381           }
382           break;
383       }
384     },
385
386     destroy: function () {
387       if ( this._$elem.is(".geo-service") ) {
388         this._$shapesContainer.geographics("destroy");
389         this._$shapesContainer = undefined;
390       } else {
391         this._created = false;
392
393         $(window).unbind("resize", this._windowHandler);
394
395         for ( var i = 0; i < this._currentServices.length; i++ ) {
396           this._currentServices[ i ].serviceContainer.geomap("destroy");
397           $.geo["_serviceTypes"][this._currentServices[i].type].destroy(this, this._$servicesContainer, this._currentServices[i]);
398         }
399
400         this._$shapesContainer.geographics("destroy");
401         this._$shapesContainer = undefined;
402         this._$drawContainer.geographics("destroy");
403         this._$drawContainer = undefined;
404
405         this._$existingChildren.detach();
406         this._$elem.html("");
407         this._$elem.append(this._$existingChildren);
408         this._$elem.removeClass("geo-map");
409       }
410
411       $.Widget.prototype.destroy.apply(this, arguments);
412     },
413
414     toMap: function (p) {
415       p = this._toMap(p);
416       return this._userGeodetic ? $.geo.proj.toGeodetic(p) : p;
417     },
418
419     toPixel: function ( p, _center /* Internal Use Only */, _pixelSize /* Internal Use Only */ ) {
420       return this._toPixel( $.geo.proj ? $.geo.proj.fromGeodetic( p ) : p, _center, _pixelSize );
421     },
422
423     opacity: function ( value, _serviceContainer ) {
424       if ( this._$elem.is( ".geo-service" ) ) {
425         this._$elem.closest( ".geo-map" ).geomap( "opacity", value, this._$elem );
426       } else {
427         if ( value >= 0 || value <= 1 ) {
428           for ( var i = 0; i < this._currentServices.length; i++ ) {
429             var service = this._currentServices[ i ];
430             if ( !_serviceContainer || service.serviceContainer[ 0 ] == _serviceContainer[ 0 ] ) {
431               service.style.opacity = value;
432               $.geo[ "_serviceTypes" ][ service.type ].opacity( this, service );
433             }
434           }
435         }
436       }
437     },
438
439     toggle: function ( value, _serviceContainer ) {
440       if ( this._$elem.is( ".geo-service" ) ) {
441         this._$elem.closest( ".geo-map" ).geomap( "toggle", value, this._$elem );
442       } else {
443
444         for ( var i = 0; i < this._currentServices.length; i++ ) {
445           var service = this._currentServices[ i ];
446
447           if ( !_serviceContainer || service.serviceContainer[ 0 ] == _serviceContainer[ 0 ] ) {
448             if ( value === undefined ) {
449               // toggle visibility
450               value = ( service.style.visibility !== "visible" );
451             }
452
453             service.style.visibility = ( value ? "visible" : "hidden" );
454
455             service.serviceContainer.toggle( value );
456
457             if ( value ) {
458               $.geo[ "_serviceTypes" ][ service.type ].refresh( this, service );
459             }
460           }
461         }
462       }
463     },
464
465     zoom: function (numberOfLevels) {
466       if (numberOfLevels != null) {
467         this._setZoom(this._options["zoom"] + numberOfLevels, false, true);
468       }
469     },
470
471     refresh: function () {
472       this._refresh();
473     },
474
475     resize: function () {
476       var size = this._findMapSize(),
477           dx = size["width"]/2 - this._contentBounds.width/2,
478           dy = size["height"]/2 - this._contentBounds.height/2,
479           i;
480
481       this._contentBounds = {
482         x: parseInt(this._$elem.css("padding-left")),
483         y: parseInt(this._$elem.css("padding-top")),
484         width: size["width"],
485         height: size["height"]
486       };
487
488       this._$resizeContainer.css( {
489         width: size["width"],
490         height: size["height"]
491       } );
492
493       for (i = 0; i < this._currentServices.length; i++) {
494         $.geo["_serviceTypes"][this._currentServices[i].type].resize(this, this._currentServices[i]);
495       }
496
497       this._$elem.find( ".geo-graphics" ).css( {
498         width: size["width"],
499         height: size["height"]
500       }).geographics( "resize" );
501
502       for (i = 0; i < this._drawPixels.length; i++) {
503         this._drawPixels[i][0] += dx;
504         this._drawPixels[i][1] += dy;
505       }
506
507       this._setCenterAndSize(this._center, this._pixelSize, false, true);
508     },
509
510     append: function ( shape, style, label, refresh ) {
511       if ( shape && $.isPlainObject( shape ) ) {
512         var shapes, arg, i, realStyle, realLabel, realRefresh;
513
514         if ( shape.type == "FeatureCollection" ) {
515           shapes = shape.features;
516         } else {
517           shapes = $.isArray( shape ) ? shape : [ shape ];
518         }
519
520         for ( i = 1; i < arguments.length; i++ ) {
521           arg = arguments[ i ];
522
523           if ( typeof arg === "object" ) {
524             realStyle = arg;
525           } else if ( typeof arg === "number" || typeof arg === "string" ) {
526             realLabel = arg;
527           } else if ( typeof arg === "boolean" ) {
528             realRefresh = arg;
529           }
530         }
531
532         for ( i = 0; i < shapes.length; i++ ) {
533           if ( shapes[ i ].type != "Point" ) {
534             var bbox = $.geo.bbox( shapes[ i ] );
535             if ( $.geo.proj && $.geo._isGeodetic( bbox ) ) {
536               bbox = $.geo.proj.fromGeodetic( bbox );
537             }
538             $.data( shapes[ i ], "geoBbox", bbox );
539           }
540
541           this._graphicShapes.push( {
542             shape: shapes[ i ],
543             style: realStyle,
544             label: realLabel
545           } );
546         }
547
548         if ( realRefresh === undefined || realRefresh ) {
549           this._refresh( );
550         }
551       }
552     },
553
554     empty: function ( refresh ) {
555       for ( var i = 0; i < this._graphicShapes.length; i++ ) {
556         $.removeData( this._graphicShapes[ i ].shape, "geoBbox" );
557       }
558
559       this._graphicShapes = [];
560
561       if ( refresh === undefined || refresh ) {
562         this._refresh();
563       }
564     },
565
566     find: function ( selector, pixelTolerance ) {
567       var isPoint = $.isPlainObject( selector ),
568           searchPixel = isPoint ? this._map.toPixel( selector.coordinates ) : undefined,
569           mapTol = this._map._pixelSize * pixelTolerance,
570           result = [],
571           graphicShape,
572           geometries,
573           curGeom,
574           i = 0;
575
576       for ( ; i < this._graphicShapes.length; i++ ) {
577         graphicShape = this._graphicShapes[ i ];
578
579         if ( isPoint ) {
580           if ( graphicShape.shape.type == "Point" ) {
581             if ( $.geo.distance( graphicShape.shape, selector ) <= mapTol ) {
582               result.push( graphicShape.shape );
583             }
584           } else {
585             var bbox = $.data( graphicShape.shape, "geoBbox" ),
586                 bboxPolygon = {
587                   type: "Polygon",
588                   coordinates: [ [
589                     [bbox[0], bbox[1]],
590                     [bbox[0], bbox[3]],
591                     [bbox[2], bbox[3]],
592                     [bbox[2], bbox[1]],
593                     [bbox[0], bbox[1]]
594                   ] ]
595                 },
596                 projectedPoint = {
597                   type: "Point",
598                   coordinates: $.geo.proj && $.geo._isGeodetic( selector.coordinates ) ? $.geo.proj.fromGeodetic( selector.coordinates ) : selector.coordinates
599                 };
600
601             if ( $.geo.distance( bboxPolygon, projectedPoint, true ) <= mapTol ) {
602               geometries = $.geo._flatten( graphicShape.shape );
603               for ( curGeom = 0; curGeom < geometries.length; curGeom++ ) {
604                 if ( $.geo.distance( geometries[ curGeom ], selector ) <= mapTol ) {
605                   result.push( graphicShape.shape );
606                   break;
607                 }
608               }
609             }
610           }
611         } else {
612           result.push( graphicShape.shape );
613         }
614       }
615
616       if ( this._$elem.is( ".geo-map" ) ) {
617         this._$elem.find( ".geo-service" ).each( function( ) {
618           result = $.merge( result, $( this ).geomap( "find", selector, pixelTolerance ) );
619         } );
620       }
621
622       return result;
623     },
624
625     remove: function ( shape, refresh ) {
626       for ( var i = 0; i < this._graphicShapes.length; i++ ) {
627         if ( this._graphicShapes[ i ].shape == shape ) {
628           $.removeData( shape, "geoBbox" );
629           var rest = this._graphicShapes.slice( i + 1 );
630           this._graphicShapes.length = i;
631           this._graphicShapes.push.apply( this._graphicShapes, rest );
632           break;
633         }
634       }
635
636       if ( refresh === undefined || refresh ) {
637         this._refresh();
638       }
639     },
640
641     _getBbox: function (center, pixelSize) {
642       center = center || this._center;
643       pixelSize = pixelSize || this._pixelSize;
644
645       // calculate the internal bbox
646       var halfWidth = this._contentBounds[ "width" ] / 2 * pixelSize,
647           halfHeight = this._contentBounds[ "height" ] / 2 * pixelSize;
648       return [ center[ 0 ] - halfWidth, center[ 1 ] - halfHeight, center[ 0 ] + halfWidth, center[ 1 ] + halfHeight ];
649     },
650
651     _setBbox: function (value, trigger, refresh) {
652       var center = [value[0] + (value[2] - value[0]) / 2, value[1] + (value[3] - value[1]) / 2],
653           pixelSize = Math.max($.geo.width(value, true) / this._contentBounds.width, $.geo.height(value, true) / this._contentBounds.height);
654
655       if (this._options["tilingScheme"]) {
656         var zoom = this._getZoom( center, pixelSize );
657         pixelSize = this._getPixelSize( zoom );
658       } else {
659         if ( this._getZoom( center, pixelSize ) < 0 ) {
660           pixelSize = this._pixelSizeMax;
661         }
662       }
663
664       this._setCenterAndSize(center, pixelSize, trigger, refresh);
665     },
666
667     _getBboxMax: function () {
668       // calculate the internal bboxMax
669       var halfWidth = this._contentBounds["width"] / 2 * this._pixelSizeMax,
670         halfHeight = this._contentBounds["height"] / 2 * this._pixelSizeMax;
671       return [this._centerMax[0] - halfWidth, this._centerMax[1] - halfHeight, this._centerMax[0] + halfWidth, this._centerMax[1] + halfHeight];
672     },
673
674     _getCenter: function () {
675       return this._center;
676     },
677
678     _getContentBounds: function () {
679       return this._contentBounds;
680     },
681
682     _getServicesContainer: function () {
683       return this._$servicesContainer;
684     },
685
686     _getZoom: function ( center, pixelSize ) {
687       center = center || this._center;
688       pixelSize = pixelSize || this._pixelSize;
689
690       // calculate the internal zoom level, vs. public zoom property
691       var tilingScheme = this._options["tilingScheme"];
692       if ( tilingScheme ) {
693         if ( tilingScheme.pixelSizes != null ) {
694           var roundedPixelSize = Math.floor(pixelSize * 1000),
695               levels = tilingScheme.pixelSizes.length,
696               i = levels - 1;
697
698           for ( ; i >= 0; i-- ) {
699             if ( Math.floor( tilingScheme.pixelSizes[ i ] * 1000 ) >= roundedPixelSize ) {
700               return i;
701             }
702           }
703
704           return 0;
705         } else {
706           return Math.max( Math.round( Math.log( tilingScheme.basePixelSize / pixelSize) / Math.log( 2 ) ), 0 );
707         }
708       } else {
709         var ratio = this._contentBounds["width"] / this._contentBounds["height"],
710             bbox = $.geo.reaspect( this._getBbox( center, pixelSize ), ratio, true ),
711             bboxMax = $.geo.reaspect(this._getBboxMax(), ratio, true);
712
713         return Math.max( Math.round( Math.log($.geo.width(bboxMax, true) / $.geo.width(bbox, true)) / Math.log(this._zoomFactor) ), 0 );
714       }
715     },
716
717     _setZoom: function ( value, trigger, refresh ) {
718       value = Math.max( value, 0 );
719
720       this._setCenterAndSize( this._center, this._getPixelSize( value ), trigger, refresh );
721     },
722
723     _createChildren: function () {
724       this._$existingChildren = this._$elem.children().detach();
725
726       this._forcePosition(this._$existingChildren);
727
728       this._$existingChildren.css("-moz-user-select", "none");
729
730       var contentSizeCss = "width:" + this._contentBounds["width"] + "px; height:" + this._contentBounds["height"] + "px; margin:0; padding:0;",
731           contentPosCss = "position:absolute; left:0; top:0;";
732
733       this._$elem.prepend('<div class="geo-event-target geo-content-frame" style="position:absolute; left:' + this._contentBounds.x + 'px; top:' + this._contentBounds.y + 'px;' + contentSizeCss + 'overflow:hidden; -khtml-user-select:none; -moz-user-select:none; -webkit-user-select:none; user-select:none;" unselectable="on"></div>');
734       this._$eventTarget = this._$contentFrame = this._$elem.children(':first');
735
736       this._$contentFrame.append('<div class="geo-services-container" style="' + contentPosCss + contentSizeCss + '"></div>');
737       this._$servicesContainer = this._$contentFrame.children(':last');
738
739       this._$contentFrame.append('<div class="geo-shapes-container" style="' + contentPosCss + contentSizeCss + '"></div>');
740       this._$shapesContainer = this._$contentFrame.children(':last');
741
742       this._$contentFrame.append( '<ul style="position: absolute; bottom: 8px; left: 8px; list-style-type: none; max-width: 50%; padding: 0; margin: 0;"></ul>' );
743       this._$attrList = this._$contentFrame.children( ":last" );
744
745       this._$contentFrame.append('<div class="geo-draw-container" style="' + contentPosCss + contentSizeCss + '"></div>');
746       this._$drawContainer = this._$contentFrame.children(':last');
747
748       this._$contentFrame.append('<div class="geo-measure-container" style="' + contentPosCss + contentSizeCss + '"><div class="geo-measure-label" style="' + contentPosCss + '; display: none;"></div></div>');
749       this._$measureContainer = this._$contentFrame.children(':last');
750       this._$measureLabel = this._$measureContainer.children();
751
752       this._$panContainer = $( [ this._$shapesContainer[ 0 ], this._$drawContainer[ 0 ], this._$measureContainer[ 0 ] ] );
753
754       this._$resizeContainer = $( [ this._$contentFrame[ 0 ], this._$servicesContainer[ 0 ], this._$eventTarget[ 0 ], this._$measureContainer[ 0 ] ] ); 
755
756       this._$contentFrame.append(this._$existingChildren);
757
758       if ( ! $("#geo-measure-style").length ) {
759         $("head").prepend( '<style type="text/css" id="geo-measure-style">.geo-measure-label { margin: 4px 0 0 6px; font-family: sans-serif;' + ( _ieVersion ? 'letter-spacing: 2px; color: #444; filter:progid:DXImageTransform.Microsoft.DropShadow(Color=white, OffX=1, OffY=2, Positive=true);' : 'color: #000; text-shadow: #fff 1px 2px; font-weight: bold;' ) + ' }</style>' );
760       }
761     },
762
763     _createServices: function () {
764       var service, i;
765
766       for ( i = 0; i < this._currentServices.length; i++ ) {
767         this._currentServices[ i ].serviceContainer.geomap( "destroy" );
768         $.geo[ "_serviceTypes" ][ this._currentServices[ i ].type ].destroy( this, this._$servicesContainer, this._currentServices[ i ] );
769       }
770
771       this._currentServices = [ ];
772       this._$servicesContainer.html( "" );
773       this._$attrList.html( "" );
774
775       for ( i = 0; i < this._options[ "services" ].length; i++ ) {
776         service = this._currentServices[ i ] = $.extend( { }, this._options[ "services" ][ i ] );
777
778         // default the service style property on our copy
779         service.style = $.extend( {
780                           visibility: "visible",
781                           opacity: 1
782                         }, service.style );
783
784         var idString = service.id ? ' id="' + service.id + '"' : "",
785             classString = 'class="geo-service ' + ( service["class"] ? service["class"] : '' ) + '"',
786             scHtml = '<div ' + idString + classString + ' style="position:absolute; left:0; top:0; width:32px; height:32px; margin:0; padding:0; display:' + ( service.style.visibility === "visible" ? "block" : "none" ) + ';"></div>',
787             servicesContainer;
788
789         this._$servicesContainer.append( scHtml );
790         serviceContainer = this._$servicesContainer.children( ":last" );
791         this._currentServices[ i ].serviceContainer = serviceContainer;
792         
793         $.geo[ "_serviceTypes" ][ service.type ].create( this, serviceContainer, service, i );
794
795         serviceContainer.data( "geoMap", this ).geomap();
796
797         if ( service.attr ) {
798           this._$attrList.append( '<li>' + service.attr + '</li>' );
799         }
800       }
801
802       this._$attrList.find( "a" ).css( {
803         position: "relative",
804         zIndex: 100
805       } );
806     },
807
808     _refreshDrawing: function ( ) {
809       this._$drawContainer.geographics("clear");
810
811       if ( this._drawPixels.length > 0 ) {
812         var mode = this._options[ "mode" ],
813             pixels = this._drawPixels,
814             coords = this._drawCoords,
815             label,
816             labelShape,
817             labelPixel,
818             widthOver,
819             heightOver;
820
821         switch ( mode ) {
822           case "measureLength":
823             mode = "drawLineString";
824             labelShape = {
825               type: "LineString",
826               coordinates: coords
827             };
828             label = $.render( { length: $.geo.length( labelShape, true ) }, "geoMeasureLength" );
829             labelPixel = $.merge( [], pixels[ pixels.length - 1 ] );
830             break;
831
832           case "measureArea":
833             mode = "drawPolygon";
834
835             labelShape = {
836               type: "Polygon",
837               coordinates: [ $.merge( [ ], coords ) ]
838             };
839             labelShape.coordinates[ 0 ].push( coords[ 0 ] );
840
841             label = $.render( { area: $.geo.area( labelShape, true ) }, "geoMeasureArea" );
842             labelPixel = $.merge( [], pixels[ pixels.length - 1 ] );
843             pixels = [ pixels ];
844             break;
845
846           case "drawPolygon":
847             pixels = [ pixels ];
848             break;
849         }
850
851         this._$drawContainer.geographics( mode, pixels );
852         
853         if ( label ) {
854           this._$measureLabel.html( label );
855
856           widthOver = this._contentBounds.width - ( this._$measureLabel.outerWidth( true ) + labelPixel[ 0 ] );
857           heightOver = this._contentBounds.height - ( this._$measureLabel.outerHeight( true ) + labelPixel[ 1 ] );
858
859           if ( widthOver < 0 ) {
860             labelPixel[ 0 ] += widthOver;
861           }
862
863           if ( heightOver < 0 ) {
864             labelPixel[ 1 ] += heightOver;
865           }
866
867           this._$measureLabel.css( {
868             left: labelPixel[ 0 ],
869             top: labelPixel[ 1 ]
870           } ).show();
871         }
872       }
873     },
874
875     _resetDrawing: function () {
876       this._drawPixels = [];
877       this._drawCoords = [];
878       this._$drawContainer.geographics("clear");
879       this._$measureLabel.hide();
880     },
881
882     _refreshShapes: function (geographics, shapes, styles, labels, center, pixelSize) {
883       var i, mgi,
884           shape,
885           shapeBbox,
886           style,
887           label,
888           hasLabel,
889           labelPixel,
890           bbox = this._map._getBbox(center, pixelSize);
891
892       for (i = 0; i < shapes.length; i++) {
893         shape = shapes[i].shape || shapes[i];
894         shape = shape.geometry || shape;
895         shapeBbox = $.data(shape, "geoBbox");
896
897         if ( shapeBbox && $.geo._bboxDisjoint( bbox, shapeBbox ) ) {
898           continue;
899         }
900
901         style = $.isArray(styles) ? styles[i].style : styles;
902         label = $.isArray(labels) ? labels[i].label : labels;
903         hasLabel = ( label !== undefined );
904         labelPixel = undefined;
905
906         switch (shape.type) {
907           case "Point":
908             labelPixel = this._map.toPixel( shape.coordinates, center, pixelSize );
909             this._$shapesContainer.geographics("drawPoint", labelPixel, style);
910             break;
911           case "LineString":
912             this._$shapesContainer.geographics("drawLineString", this._map.toPixel(shape.coordinates, center, pixelSize), style);
913             if ( hasLabel ) {
914               labelPixel = this._map.toPixel( $.geo.pointAlong( shape, .5 ).coordinates, center, pixelSize );
915             }
916             break;
917           case "Polygon":
918             this._$shapesContainer.geographics("drawPolygon", this._map.toPixel(shape.coordinates, center, pixelSize), style);
919             if ( hasLabel ) {
920               labelPixel = this._map.toPixel( $.geo.centroid( shape ).coordinates, center, pixelSize );
921             }
922             break;
923           case "MultiPoint":
924             for (mgi = 0; mgi < shape.coordinates.length; mgi++) {
925               this._$shapesContainer.geographics("drawPoint", this._map.toPixel(shape.coordinates[mgi], center, pixelSize), style);
926             }
927             if ( hasLabel ) {
928               labelPixel = this._map.toPixel( $.geo.centroid( shape ).coordinates, center, pixelSize );
929             }
930             break;
931           case "MultiLineString":
932             for (mgi = 0; mgi < shape.coordinates.length; mgi++) {
933               this._$shapesContainer.geographics("drawLineString", this._map.toPixel(shape.coordinates[mgi], center, pixelSize), style);
934             }
935             if ( hasLabel ) {
936               labelPixel = this._map.toPixel( $.geo.centroid( shape ).coordinates, center, pixelSize );
937             }
938             break;
939           case "MultiPolygon":
940             for (mgi = 0; mgi < shape.coordinates.length; mgi++) {
941               this._$shapesContainer.geographics("drawPolygon", this._map.toPixel(shape.coordinates[mgi], center, pixelSize), style);
942             }
943             if ( hasLabel ) {
944               labelPixel = this._map.toPixel( $.geo.centroid( shape ).coordinates, center, pixelSize );
945             }
946             break;
947
948           case "GeometryCollection":
949             this._refreshShapes(geographics, shape.geometries, style, label, center, pixelSize);
950             break;
951         }
952
953         if ( hasLabel && labelPixel ) {
954           this._$shapesContainer.geographics( "drawLabel", labelPixel, label );
955         }
956       }
957     },
958
959     _findMapSize: function () {
960       // really, really attempt to find a size for this thing
961       // even if it's hidden (look at parents)
962       var size = { width: 0, height: 0 },
963         sizeContainer = this._$elem;
964
965       while (sizeContainer.size() && !(size["width"] > 0 && size["height"] > 0)) {
966         size = { width: sizeContainer.width(), height: sizeContainer.height() };
967         if (size["width"] <= 0 || size["height"] <= 0) {
968           size = { width: parseInt(sizeContainer.css("width")), height: parseInt(sizeContainer.css("height")) };
969         }
970         sizeContainer = sizeContainer.parent();
971       }
972       return size;
973     },
974
975     _forcePosition: function (elem) {
976       var cssPosition = elem.css("position");
977       if (cssPosition != "relative" && cssPosition != "absolute" && cssPosition != "fixed") {
978         elem.css("position", "relative");
979       }
980     },
981
982     _getPixelSize: function ( zoom ) {
983       var tilingScheme = this._options["tilingScheme"];
984       if (tilingScheme != null) {
985         if (zoom === 0) {
986           return tilingScheme.pixelSizes != null ? tilingScheme.pixelSizes[0] : tilingScheme.basePixelSize;
987         }
988
989         zoom = Math.round(zoom);
990         zoom = Math.max(zoom, 0);
991         var levels = tilingScheme.pixelSizes != null ? tilingScheme.pixelSizes.length : tilingScheme.levels;
992         zoom = Math.min(zoom, levels - 1);
993
994         if (tilingScheme.pixelSizes != null) {
995           return tilingScheme.pixelSizes[zoom];
996         } else {
997           return tilingScheme.basePixelSize / Math.pow(2, zoom);
998         }
999       } else {
1000         var bbox = $.geo.scaleBy( this._getBboxMax(), 1 / Math.pow( this._zoomFactor, zoom ), true );
1001         return Math.max( $.geo.width( bbox, true ) / this._contentBounds.width, $.geo.height( bbox, true ) / this._contentBounds.height );
1002       }
1003     },
1004
1005     _getZoomCenterAndSize: function ( anchor, zoomDelta, full ) {
1006       var zoomFactor = ( full ? this._fullZoomFactor : this._partialZoomFactor ),
1007           scale = Math.pow( zoomFactor, -zoomDelta ),
1008           pixelSize,
1009           zoomLevel;
1010
1011       if ( this._options[ "tilingScheme" ] ) {
1012         zoomLevel = this._getZoom(this._center, this._pixelSize * scale);
1013         pixelSize = this._getPixelSize(zoomLevel);
1014       } else {
1015         pixelSize = this._pixelSize * scale;
1016
1017         if ( this._getZoom( this._center, pixelSize ) < 0 ) {
1018           pixelSize = this._pixelSizeMax;
1019         }
1020       }
1021
1022       var ratio = pixelSize / this._pixelSize,
1023           anchorMapCoord = this._toMap(anchor),
1024           centerDelta = [(this._center[0] - anchorMapCoord[0]) * ratio, (this._center[1] - anchorMapCoord[1]) * ratio],
1025           scaleCenter = [anchorMapCoord[0] + centerDelta[0], anchorMapCoord[1] + centerDelta[1]];
1026
1027       return { pixelSize: pixelSize, center: scaleCenter };
1028     },
1029
1030     _mouseWheelFinish: function () {
1031       this._wheelTimeout = null;
1032
1033       if (this._wheelLevel != 0) {
1034         var wheelCenterAndSize = this._getZoomCenterAndSize( this._anchor, this._wheelLevel, this._options[ "tilingScheme" ] != null );
1035
1036         this._setCenterAndSize(wheelCenterAndSize.center, wheelCenterAndSize.pixelSize, true, true);
1037
1038         this._wheelLevel = 0;
1039       } else {
1040         this._refresh();
1041       }
1042     },
1043
1044     _panEnd: function () {
1045       this._velocity = [
1046         (this._velocity[0] > 0 ? Math.floor(this._velocity[0] * this._friction[0]) : Math.ceil(this._velocity[0] * this._friction[0])),
1047         (this._velocity[1] > 0 ? Math.floor(this._velocity[1] * this._friction[1]) : Math.ceil(this._velocity[1] * this._friction[1]))
1048       ];
1049
1050       if (Math.abs(this._velocity[0]) < 4 && Math.abs(this._velocity[1]) < 4) {
1051         this._panFinalize();
1052       } else {
1053         this._current = [
1054           this._current[0] + this._velocity[0],
1055           this._current[1] + this._velocity[1]
1056         ];
1057
1058         this._panMove();
1059         setTimeout($.proxy(this._panEnd, this), 30);
1060       }
1061     },
1062
1063     _panFinalize: function () {
1064       if (this._panning) {
1065         this._velocity = [0, 0];
1066
1067         var dx = this._current[0] - this._anchor[0],
1068             dy = this._current[1] - this._anchor[1],
1069             image = this._options[ "axisLayout" ] === "image",
1070             dxMap = -dx * this._pixelSize,
1071             dyMap = ( image ? -1 : 1 ) * dy * this._pixelSize;
1072
1073         this._$panContainer.css({ left: 0, top: 0 });
1074
1075         this._$servicesContainer.find( ".geo-shapes-container" ).css( { left: 0, top: 0 } );
1076
1077         this._setCenterAndSize([this._center[0] + dxMap, this._center[1] + dyMap], this._pixelSize, true, true);
1078
1079         this._$eventTarget.css("cursor", this._options["cursors"][this._options["mode"]]);
1080
1081         this._inOp = false;
1082         this._anchor = this._current;
1083         this._mouseDown = this._toolPan = this._panning = false;
1084       }
1085     },
1086
1087     _panMove: function () {
1088       if ( ! this._options[ "pannable" ] ) {
1089         return;
1090       }
1091
1092       var dx = this._current[0] - this._lastDrag[0],
1093           dy = this._current[1] - this._lastDrag[1],
1094           i = 0,
1095           service,
1096           translateObj;
1097
1098       if (this._toolPan || dx > 3 || dx < -3 || dy > 3 || dy < -3) {
1099         if (!this._toolPan) {
1100           this._toolPan = true;
1101           this._$eventTarget.css("cursor", this._options["cursors"]["pan"]);
1102         }
1103
1104         if (this._mouseDown) {
1105           this._velocity = [dx, dy];
1106         }
1107
1108         if (dx != 0 || dy != 0) {
1109           this._panning = true;
1110           this._lastDrag = this._current;
1111
1112           translateObj = {
1113             left: function (index, value) {
1114               return parseInt(value) + dx;
1115             },
1116             top: function (index, value) {
1117               return parseInt(value) + dy;
1118             }
1119           };
1120
1121           for ( i = 0; i < this._currentServices.length; i++ ) {
1122             service = this._currentServices[ i ];
1123             $.geo[ "_serviceTypes" ][ service.type ].interactivePan( this, service, dx, dy );
1124             
1125             service.serviceContainer.find( ".geo-shapes-container" ).css( translateObj );
1126           }
1127
1128           this._$panContainer.css( translateObj );
1129
1130           //this._refreshDrawing();
1131         }
1132       }
1133     },
1134
1135     _refresh: function () {
1136       var service,
1137           i = 0;
1138
1139       if ( this._$elem.is( ".geo-map" ) ) {
1140         for ( ; i < this._currentServices.length; i++ ) {
1141           service = this._currentServices[ i ];
1142
1143           if ( !this._mouseDown && $.geo[ "_serviceTypes" ][ service.type ] !== null ) {
1144             $.geo[ "_serviceTypes" ][ service.type ].refresh( this, service );
1145             service.serviceContainer.geomap( "refresh" );
1146           }
1147         }
1148       }
1149
1150       if ( this._$shapesContainer ) {
1151         this._$shapesContainer.geographics( "clear" );
1152         if ( this._graphicShapes.length > 0 ) {
1153           this._refreshShapes( this._$shapesContainer, this._graphicShapes, this._graphicShapes, this._graphicShapes );
1154         }
1155       }
1156     },
1157
1158     _setCenterAndSize: function (center, pixelSize, trigger, refresh) {
1159       if ( ! $.isArray( center ) || center.length != 2 || typeof center[ 0 ] !== "number" || typeof center[ 1 ] !== "number" ) {
1160         return;
1161       }
1162
1163       // the final call during any extent change
1164       if (this._pixelSize != pixelSize) {
1165         this._$elem.find( ".geo-shapes-container" ).geographics("clear");
1166         for (var i = 0; i < this._currentServices.length; i++) {
1167           var service = this._currentServices[i];
1168           $.geo["_serviceTypes"][service.type].interactiveScale(this, service, center, pixelSize);
1169         }
1170       }
1171
1172       this._center = $.merge( [ ], center );
1173       this._options["pixelSize"] = this._pixelSize = pixelSize;
1174
1175       if ( this._userGeodetic ) {
1176         this._options["bbox"] = $.geo.proj.toGeodetic( this._getBbox() );
1177         this._options["center"] = $.geo.proj.toGeodetic( this._center );
1178       } else {
1179         this._options["bbox"] = this._getBbox();
1180         this._options["center"] = $.merge( [ ], center );
1181       }
1182
1183       this._options["zoom"] = this._getZoom();
1184
1185       if (this._drawCoords.length > 0) {
1186         this._drawPixels = this._toPixel(this._drawCoords);
1187       }
1188
1189       if (trigger) {
1190         this._trigger("bboxchange", window.event, { bbox: $.merge( [ ], this._options["bbox"] ) });
1191       }
1192
1193       if (refresh) {
1194         this._refresh();
1195         this._refreshDrawing();
1196       }
1197     },
1198
1199     _toMap: function (p, center, pixelSize) {
1200       // ignores $.geo.proj
1201
1202       center = center || this._center;
1203       pixelSize = pixelSize || this._pixelSize;
1204
1205       var isMultiPointOrLineString = $.isArray( p[ 0 ] ),
1206           isMultiLineStringOrPolygon = isMultiPointOrLineString && $.isArray( p[ 0 ][ 0 ] ),
1207           isMultiPolygon = isMultiLineStringOrPolygon && $.isArray( p[ 0 ][ 0 ][ 0 ] ),
1208           width = this._contentBounds["width"],
1209           height = this._contentBounds["height"],
1210           halfWidth = width / 2 * pixelSize,
1211           halfHeight = height / 2 * pixelSize,
1212           bbox = [center[0] - halfWidth, center[1] - halfHeight, center[0] + halfWidth, center[1] + halfHeight],
1213           xRatio = $.geo.width(bbox, true) / width,
1214           yRatio = $.geo.height(bbox, true) / height,
1215           yOffset,
1216           image = this._options[ "axisLayout" ] === "image",
1217           result = [],
1218           i, j, k;
1219
1220       if ( !isMultiPolygon ) {
1221         if ( !isMultiLineStringOrPolygon ) {
1222           if ( !isMultiPointOrLineString ) {
1223             p = [ p ];
1224           }
1225           p = [ p ];
1226         }
1227         p = [ p ];
1228       }
1229
1230       for ( i = 0; i < p.length; i++ ) {
1231         result[ i ] = [ ];
1232         for ( j = 0; j < p[ i ].length; j++ ) {
1233           result[ i ][ j ] = [ ];
1234           for ( k = 0; k < p[ i ][ j ].length; k++ ) {
1235             yOffset = (p[ i ][ j ][ k ][1] * yRatio);
1236             result[ i ][ j ][ k ] = [
1237               bbox[ 0 ] + ( p[ i ][ j ][ k ][ 0 ] * xRatio ),
1238               image ? bbox[ 1 ] + yOffset : bbox[ 3 ] - yOffset
1239             ];
1240           }
1241         }
1242       }
1243
1244       return isMultiPolygon ? result : isMultiLineStringOrPolygon ? result[ 0 ] : isMultiPointOrLineString ? result[ 0 ][ 0 ] : result[ 0 ][ 0 ][ 0 ];
1245     },
1246
1247     _toPixel: function (p, center, pixelSize) {
1248       // ignores $.geo.proj
1249
1250       center = center || this._center;
1251       pixelSize = pixelSize || this._pixelSize;
1252
1253       var isMultiPointOrLineString = $.isArray( p[ 0 ] ),
1254           isMultiLineStringOrPolygon = isMultiPointOrLineString && $.isArray( p[ 0 ][ 0 ] ),
1255           isMultiPolygon = isMultiLineStringOrPolygon && $.isArray( p[ 0 ][ 0 ][ 0 ] ),
1256           width = this._contentBounds["width"],
1257           height = this._contentBounds["height"],
1258           halfWidth = width / 2 * pixelSize,
1259           halfHeight = height / 2 * pixelSize,
1260           bbox = [center[0] - halfWidth, center[1] - halfHeight, center[0] + halfWidth, center[1] + halfHeight],
1261           bboxWidth = $.geo.width(bbox, true),
1262           bboxHeight = $.geo.height(bbox, true),
1263           image = this._options[ "axisLayout" ] === "image",
1264           xRatio = width / bboxWidth,
1265           yRatio = height / bboxHeight,
1266           result = [ ],
1267           i, j, k;
1268
1269       if ( !isMultiPolygon ) {
1270         if ( !isMultiLineStringOrPolygon ) {
1271           if ( !isMultiPointOrLineString ) {
1272             p = [ p ];
1273           }
1274           p = [ p ];
1275         }
1276         p = [ p ];
1277       }
1278
1279       for ( i = 0; i < p.length; i++ ) {
1280         result[ i ] = [ ];
1281         for ( j = 0; j < p[ i ].length; j++ ) {
1282           result[ i ][ j ] = [ ];
1283           for ( k = 0; k < p[ i ][ j ].length; k++ ) {
1284             result[ i ][ j ][ k ] = [
1285               Math.round( ( p[ i ][ j ][ k ][ 0 ] - bbox[ 0 ] ) * xRatio ),
1286               Math.round( ( image ? p[ i ][ j ][ k ][ 1 ] - bbox[ 1 ] : bbox[ 3 ] - p[ i ][ j ][ k ][ 1 ] ) * yRatio )
1287             ];
1288           }
1289         }
1290       }
1291
1292       return isMultiPolygon ? result : isMultiLineStringOrPolygon ? result[ 0 ] : isMultiPointOrLineString ? result[ 0 ][ 0 ] : result[ 0 ][ 0 ][ 0 ];
1293     },
1294
1295     _zoomTo: function (coord, zoom, trigger, refresh) {
1296       zoom = zoom < 0 ? 0 : zoom;
1297
1298       var pixelSize = this._getPixelSize( zoom );
1299
1300       this._setCenterAndSize( coord, pixelSize, trigger, refresh );
1301     },
1302
1303     _document_keydown: function (e) {
1304       var len = this._drawCoords.length;
1305       if (len > 0 && e.which == 27) {
1306         if (len <= 2) {
1307           this._resetDrawing();
1308           this._inOp = false;
1309         } else {
1310           this._drawCoords[len - 2] = $.merge( [], this._drawCoords[ len - 1 ] );
1311           this._drawPixels[len - 2] = $.merge( [], this._drawPixels[ len - 1 ] );
1312
1313           this._drawCoords.length--;
1314           this._drawPixels.length--;
1315
1316           this._refreshDrawing();
1317         }
1318       }
1319     },
1320
1321     _eventTarget_dblclick_zoom: function(e) {
1322       this._trigger("dblclick", e, { type: "Point", coordinates: this.toMap(this._current) });
1323       if (!e.isDefaultPrevented()) {
1324         var centerAndSize = this._getZoomCenterAndSize(this._current, 1, true );
1325         this._setCenterAndSize(centerAndSize.center, centerAndSize.pixelSize, true, true);
1326       }
1327     },
1328
1329     _eventTarget_dblclick: function (e) {
1330       if ( this._options[ "mode" ] === "static" ) {
1331         return;
1332       }
1333
1334       this._panFinalize();
1335
1336       if (this._drawTimeout) {
1337         window.clearTimeout(this._drawTimeout);
1338         this._drawTimeout = null;
1339       }
1340
1341       var offset = $(e.currentTarget).offset();
1342
1343       switch (this._options["mode"]) {
1344         case "drawLineString":
1345           if ( this._drawCoords.length > 1 && ! ( this._drawCoords[0][0] == this._drawCoords[1][0] &&
1346                                                   this._drawCoords[0][1] == this._drawCoords[1][1] ) ) {
1347               this._drawCoords.length--;
1348               this._trigger( "shape", e, {
1349                 type: "LineString",
1350                 coordinates: this._userGeodetic ? $.geo.proj.toGeodetic(this._drawCoords) : this._drawCoords
1351               } );
1352           } else {
1353             this._eventTarget_dblclick_zoom(e);
1354           }
1355           this._resetDrawing();
1356           break;
1357
1358         case "drawPolygon":
1359           if ( this._drawCoords.length > 1 && ! ( this._drawCoords[0][0] == this._drawCoords[1][0] &&
1360                                                   this._drawCoords[0][1] == this._drawCoords[1][1] ) ) {
1361             var endIndex = this._drawCoords.length - 1;
1362             if (endIndex > 2) {
1363               this._drawCoords[endIndex] = $.merge( [], this._drawCoords[0] );
1364               this._trigger( "shape", e, {
1365                 type: "Polygon",
1366                 coordinates: [ this._userGeodetic ? $.geo.proj.toGeodetic(this._drawCoords) : this._drawCoords ]
1367               } );
1368             }
1369           } else {
1370             this._eventTarget_dblclick_zoom(e);
1371           }
1372           this._resetDrawing();
1373           break;
1374
1375         case "measureLength":
1376         case "measureArea":
1377           this._resetDrawing();
1378           break;
1379
1380         default:
1381           this._eventTarget_dblclick_zoom(e);
1382           break;
1383       }
1384
1385       this._inOp = false;
1386     },
1387
1388     _eventTarget_touchstart: function (e) {
1389       if ( this._options[ "mode" ] === "static" ) {
1390         return;
1391       }
1392
1393       if ( !this._supportTouch && e.which != 1 ) {
1394         return;
1395       }
1396
1397       this._panFinalize();
1398       this._mouseWheelFinish();
1399
1400       var offset = $(e.currentTarget).offset(),
1401           touches = e.originalEvent.changedTouches;
1402
1403       if ( this._supportTouch ) {
1404         this._multiTouchAnchor = $.merge( [ ], touches );
1405
1406         this._isMultiTouch = this._multiTouchAnchor.length > 1;
1407
1408         if ( this._isMultiTouch ) {
1409           this._multiTouchCurrentBbox = [
1410             touches[0].pageX - offset.left,
1411             touches[0].pageY - offset.top,
1412             touches[1].pageX - offset.left,
1413             touches[1].pageY - offset.top
1414           ];
1415
1416           this._multiTouchAnchorBbox = $.merge( [ ], this._multiTouchCurrentBbox );
1417
1418           this._current = $.geo.center( this._multiTouchCurrentBbox, true );
1419         } else {
1420           this._multiTouchCurrentBbox = [
1421             touches[0].pageX - offset.left,
1422             touches[0].pageY - offset.top,
1423             NaN,
1424             NaN
1425           ];
1426
1427           this._current = [ touches[0].pageX - offset.left, touches[0].pageY - offset.top ];
1428         }
1429       } else {
1430         this._current = [e.pageX - offset.left, e.pageY - offset.top];
1431       }
1432
1433       if (this._softDblClick) {
1434         var downDate = $.now();
1435         if (downDate - this._downDate < 750) {
1436           if (this._isTap) {
1437             var dx = this._current[0] - this._anchor[0],
1438                 dy = this._current[1] - this._anchor[1],
1439                 distance = Math.sqrt((dx * dx) + (dy * dy));
1440             if (distance > 8) {
1441               this._isTap = false;
1442             } else {
1443               this._current = $.merge( [ ], this._anchor );
1444             }
1445           }
1446
1447           if (this._isDbltap) {
1448             this._isDbltap = false;
1449           } else {
1450             this._isDbltap = this._isTap;
1451           }
1452         } else {
1453           this._isDbltap = false;
1454         }
1455         this._isTap = true;
1456         this._downDate = downDate;
1457       }
1458
1459       this._mouseDown = true;
1460       this._anchor = $.merge( [ ], this._current );
1461
1462       if (!this._inOp && e.shiftKey) {
1463         this._shiftZoom = true;
1464         this._$eventTarget.css("cursor", this._options["cursors"]["zoom"]);
1465       } else if ( !this._isMultiTouch && this._options[ "pannable" ] ) {
1466         this._inOp = true;
1467
1468         switch (this._options["mode"]) {
1469           case "zoom":
1470             break;
1471
1472           default:
1473             this._lastDrag = this._current;
1474
1475             if (e.currentTarget.setCapture) {
1476               e.currentTarget.setCapture();
1477             }
1478
1479             break;
1480         }
1481       }
1482
1483       e.preventDefault();
1484       return false;
1485     },
1486
1487     _dragTarget_touchmove: function (e) {
1488       if ( this._options[ "mode" ] === "static" ) {
1489         return;
1490       }
1491
1492       var offset = this._$eventTarget.offset(),
1493           drawCoordsLen = this._drawCoords.length,
1494           touches = e.originalEvent.changedTouches,
1495           current,
1496           service,
1497           i = 0;
1498
1499       if ( this._supportTouch ) {
1500         if ( !this._isMultiTouch && touches[ 0 ].identifier !== this._multiTouchAnchor[ 0 ].identifier ) {
1501           // switch to multitouch
1502           this._mouseDown = false;
1503           this._dragTarget_touchstop( e );
1504
1505           this._isMultiTouch = true;
1506
1507           this._multiTouchAnchor.push( touches[ 0 ] );
1508
1509           this._multiTouchCurrentBbox = [
1510             this._multiTouchCurrentBbox[ 0 ],
1511             this._multiTouchCurrentBbox[ 1 ],
1512             this._multiTouchAnchor[1].pageX - offset.left,
1513             this._multiTouchAnchor[1].pageY - offset.top
1514           ];
1515
1516           this._multiTouchAnchorBbox = $.merge( [ ], this._multiTouchCurrentBbox );
1517
1518           this._mouseDown = true;
1519           this._anchor = this._current = $.geo.center( this._multiTouchCurrentBbox, true );
1520
1521           return false;
1522         }
1523
1524         if ( this._isMultiTouch ) {
1525           for ( ; i < touches.length; i++ ) {
1526             if ( touches[ i ].identifier === this._multiTouchAnchor[ 0 ].identifier ) {
1527               this._multiTouchCurrentBbox[ 0 ] = touches[ i ].pageX - offset.left;
1528               this._multiTouchCurrentBbox[ 1 ] = touches[ i ].pageY - offset.top;
1529             } else if ( touches[ i ].identifier === this._multiTouchAnchor[ 1 ].identifier ) {
1530               this._multiTouchCurrentBbox[ 2 ] = touches[ i ].pageX - offset.left;
1531               this._multiTouchCurrentBbox[ 3 ] = touches[ i ].pageY - offset.top;
1532             }
1533           }
1534
1535           current = $.geo.center( this._multiTouchCurrentBbox, true );
1536
1537           var currentWidth = this._multiTouchCurrentBbox[ 2 ] - this._multiTouchCurrentBbox[ 0 ],
1538               anchorWidth = this._multiTouchAnchorBbox[ 2 ] - this._multiTouchAnchorBbox[ 0 ],
1539               ratioWidth = currentWidth / anchorWidth;
1540
1541           this._wheelLevel = Math.abs( Math.floor( ( 1 - ratioWidth ) * 10 ) );
1542           if ( Math.abs( currentWidth ) < Math.abs( anchorWidth ) ) {
1543             this._wheelLevel = - this._wheelLevel;
1544           }
1545
1546           var pinchCenterAndSize = this._getZoomCenterAndSize( this._anchor, this._wheelLevel, false );
1547           this._$elem.find( ".geo-shapes-container" ).geographics("clear");
1548
1549           for ( i = 0; i < this._currentServices.length; i++ ) {
1550             service = this._currentServices[ i ];
1551             $.geo[ "_serviceTypes" ][ service.type ].interactiveScale( this, service, pinchCenterAndSize.center, pinchCenterAndSize.pixelSize );
1552           }
1553
1554           if (this._graphicShapes.length > 0 && this._graphicShapes.length < 256) {
1555             this._refreshShapes(this._$shapesContainer, this._graphicShapes, this._graphicShapes, this._graphicShapes, pinchCenterAndSize.center, pinchCenterAndSize.pixelSize);
1556           }
1557
1558
1559           if (this._drawCoords.length > 0) {
1560             this._drawPixels = this._toPixel(this._drawCoords, pinchCenterAndSize.center, pinchCenterAndSize.pixelSize);
1561             this._refreshDrawing();
1562           }
1563
1564           current = $.geo.center( this._multiTouchCurrentBbox, true );
1565         } else {
1566           current = [e.originalEvent.changedTouches[0].pageX - offset.left, e.originalEvent.changedTouches[0].pageY - offset.top];
1567         }
1568       } else {
1569         current = [e.pageX - offset.left, e.pageY - offset.top];
1570       }
1571
1572       if (current[0] === this._lastMove[0] && current[1] === this._lastMove[1]) {
1573         if ( this._inOp ) {
1574           e.preventDefault();
1575           return false;
1576         }
1577       }
1578
1579       if ( _ieVersion == 7 ) {
1580         this._isDbltap = this._isTap = false;
1581       }
1582
1583       if (this._mouseDown) {
1584         this._current = current;
1585         this._moveDate = $.now();
1586       }
1587
1588       if ( this._isMultiTouch ) {
1589         e.preventDefault( );
1590         this._isDbltap = this._isTap = false;
1591         return false;
1592       }
1593
1594       var mode = this._shiftZoom ? "zoom" : this._options["mode"];
1595
1596       switch (mode) {
1597         case "zoom":
1598           if ( this._mouseDown ) {
1599             this._$drawContainer.geographics( "clear" );
1600             this._$drawContainer.geographics( "drawBbox", [
1601               this._anchor[ 0 ],
1602               this._anchor[ 1 ],
1603               current[ 0 ],
1604               current[ 1 ]
1605             ] );
1606           } else {
1607             this._trigger("move", e, { type: "Point", coordinates: this.toMap(current) });
1608           }
1609           break;
1610
1611         case "drawLineString":
1612         case "drawPolygon":
1613         case "measureLength":
1614         case "measureArea":
1615           if (this._mouseDown || this._toolPan) {
1616             this._panMove();
1617           } else {
1618             if (drawCoordsLen > 0) {
1619               this._drawCoords[drawCoordsLen - 1] = this._toMap(current);
1620               this._drawPixels[drawCoordsLen - 1] = current;
1621
1622               this._refreshDrawing();
1623             }
1624
1625             this._trigger("move", e, { type: "Point", coordinates: this.toMap(current) });
1626           }
1627           break;
1628
1629         default:
1630           if (this._mouseDown || this._toolPan) {
1631             this._panMove();
1632           } else {
1633             this._trigger("move", e, { type: "Point", coordinates: this.toMap(current) });
1634           }
1635           break;
1636       }
1637
1638       this._lastMove = current;
1639
1640       if ( this._inOp ) {
1641         e.preventDefault();
1642         return false;
1643       }
1644     },
1645
1646     _dragTarget_touchstop: function (e) {
1647       if ( this._options[ "mode" ] === "static" ) {
1648         return;
1649       }
1650
1651       if (!this._mouseDown && _ieVersion == 7) {
1652         // ie7 doesn't appear to trigger dblclick on this._$eventTarget,
1653         // we fake regular click here to cause soft dblclick
1654         this._eventTarget_touchstart(e);
1655       }
1656
1657       var mouseWasDown = this._mouseDown,
1658           wasToolPan = this._toolPan,
1659           offset = this._$eventTarget.offset(),
1660           mode = this._shiftZoom ? "zoom" : this._options["mode"],
1661           current, i, clickDate,
1662           dx, dy;
1663
1664       if (this._supportTouch) {
1665         current = [e.originalEvent.changedTouches[0].pageX - offset.left, e.originalEvent.changedTouches[0].pageY - offset.top];
1666       } else {
1667         current = [e.pageX - offset.left, e.pageY - offset.top];
1668       }
1669
1670       if (this._softDblClick) {
1671         if (this._isTap) {
1672           var dx = current[0] - this._anchor[0],
1673               dy = current[1] - this._anchor[1],
1674               distance = Math.sqrt((dx * dx) + (dy * dy));
1675           if (distance <= 8) {
1676             current = $.merge( [ ], this._anchor );
1677           }
1678         }
1679       }
1680
1681       dx = current[0] - this._anchor[0];
1682       dy = current[1] - this._anchor[1];
1683
1684       this._$eventTarget.css("cursor", this._options["cursors"][this._options["mode"]]);
1685
1686       this._shiftZoom = this._mouseDown = this._toolPan = false;
1687
1688       if ( this._isMultiTouch ) {
1689         e.preventDefault( );
1690         this._isMultiTouch = false;
1691
1692         var pinchCenterAndSize = this._getZoomCenterAndSize( this._anchor, this._wheelLevel, false );
1693
1694         this._setCenterAndSize(pinchCenterAndSize.center, pinchCenterAndSize.pixelSize, true, true);
1695
1696         this._wheelLevel = 0;
1697
1698         return false;
1699       }
1700
1701       if (document.releaseCapture) {
1702         document.releaseCapture();
1703       }
1704
1705       if (mouseWasDown) {
1706         clickDate = $.now();
1707         this._current = current;
1708
1709         switch (mode) {
1710           case "zoom":
1711             if ( dx > 0 || dy > 0 ) {
1712               var minSize = this._pixelSize * 6,
1713                   bboxCoords = this._toMap( [ [
1714                       Math.min( this._anchor[ 0 ], current[ 0 ] ),
1715                       Math.max( this._anchor[ 1 ], current[ 1 ] )
1716                     ], [
1717                       Math.max( this._anchor[ 0 ], current[ 0 ] ),
1718                       Math.min( this._anchor[ 1 ], current[ 1 ] )
1719                     ]
1720                   ] ),
1721                   bbox = [
1722                     bboxCoords[0][0],
1723                     bboxCoords[0][1],
1724                     bboxCoords[1][0],
1725                     bboxCoords[1][1]
1726                   ];
1727
1728               if ( ( bbox[2] - bbox[0] ) < minSize && ( bbox[3] - bbox[1] ) < minSize ) {
1729                 bbox = $.geo.scaleBy( this._getBbox( $.geo.center( bbox, true ) ), .5, true );
1730               }
1731
1732               this._setBbox(bbox, true, true);
1733             }
1734
1735             this._resetDrawing();
1736             break;
1737
1738           case "drawPoint":
1739             if (this._drawTimeout) {
1740               window.clearTimeout(this._drawTimeout);
1741               this._drawTimeout = null;
1742             }
1743
1744             if (wasToolPan) {
1745               this._panFinalize();
1746             } else {
1747               if (clickDate - this._clickDate > 100) {
1748                 var geomap = this;
1749                 this._drawTimeout = setTimeout(function () {
1750                   if (geomap._drawTimeout) {
1751                     geomap._trigger("shape", e, { type: "Point", coordinates: geomap.toMap(current) });
1752                     geomap._inOp = false;
1753                     geomap._drawTimeout = null;
1754                   }
1755                 }, 250);
1756               }
1757             }
1758             break;
1759
1760           case "drawLineString":
1761           case "drawPolygon":
1762           case "measureLength":
1763           case "measureArea":
1764             if (wasToolPan) {
1765               this._panFinalize();
1766             } else {
1767               i = (this._drawCoords.length == 0 ? 0 : this._drawCoords.length - 1);
1768
1769               this._drawCoords[i] = this._toMap(current);
1770               this._drawPixels[i] = current;
1771
1772               if (i < 2 || !(this._drawCoords[i][0] == this._drawCoords[i-1][0] &&
1773                              this._drawCoords[i][1] == this._drawCoords[i-1][1])) {
1774                 this._drawCoords[i + 1] = this._toMap(current);
1775                 this._drawPixels[i + 1] = current;
1776               }
1777
1778               this._refreshDrawing();
1779             }
1780             break;
1781
1782           default:
1783             if (wasToolPan) {
1784               this._panEnd();
1785             } else {
1786               if (clickDate - this._clickDate > 100) {
1787                 this._trigger("click", e, { type: "Point", coordinates: this.toMap(current) });
1788                 this._inOp = false;
1789               }
1790             }
1791             break;
1792         }
1793
1794         this._clickDate = clickDate;
1795
1796         if (this._softDblClick && this._isDbltap) {
1797           this._isDbltap = this._isTap = false;
1798           this._$eventTarget.trigger("dblclick", e);
1799         }
1800       }
1801
1802       if ( this._inOp ) {
1803         e.preventDefault();
1804         return false;
1805       }
1806     },
1807
1808     _eventTarget_mousewheel: function (e, delta) {
1809       if ( this._options[ "mode" ] === "static" || this._options[ "scroll" ] === "off" ) {
1810         return;
1811       }
1812
1813       e.preventDefault();
1814
1815       this._panFinalize();
1816
1817       if ( this._mouseDown ) {
1818         return false;
1819       }
1820
1821       if (delta != 0) {
1822         if (this._wheelTimeout) {
1823           window.clearTimeout(this._wheelTimeout);
1824           this._wheelTimeout = null;
1825         } else {
1826           var offset = $(e.currentTarget).offset();
1827           this._anchor = [e.pageX - offset.left, e.pageY - offset.top];
1828         }
1829
1830         this._wheelLevel += delta;
1831
1832         var wheelCenterAndSize = this._getZoomCenterAndSize( this._anchor, this._wheelLevel, this._options[ "tilingScheme" ] != null ),
1833             service,
1834             i = 0;
1835
1836         this._$elem.find( ".geo-shapes-container" ).geographics("clear");
1837
1838         for ( ; i < this._currentServices.length; i++ ) {
1839           service = this._currentServices[ i ];
1840           $.geo["_serviceTypes"][service.type].interactiveScale(this, service, wheelCenterAndSize.center, wheelCenterAndSize.pixelSize);
1841         }
1842
1843         if (this._graphicShapes.length > 0 && this._graphicShapes.length < 256) {
1844           this._refreshShapes(this._$shapesContainer, this._graphicShapes, this._graphicShapes, this._graphicShapes, wheelCenterAndSize.center, wheelCenterAndSize.pixelSize);
1845         }
1846
1847         if (this._drawCoords.length > 0) {
1848           this._drawPixels = this._toPixel(this._drawCoords, wheelCenterAndSize.center, wheelCenterAndSize.pixelSize);
1849           this._refreshDrawing();
1850         }
1851
1852         var geomap = this;
1853         this._wheelTimeout = window.setTimeout(function () {
1854           geomap._mouseWheelFinish();
1855         }, 1000);
1856       }
1857
1858       return false;
1859     }
1860   }
1861   );
1862 })(jQuery);
1863
1864