﻿/**
* @name Key Drag Zoom for V3
* @version 1.0
* @author: Nianwei Liu [nianwei at gmail dot com] & Gary Little [gary at luxcentral dot com]
* @fileoverview This library adds a drag zoom capability to a Google map.
*  When drag zoom is enabled, holding down a user-defined hot key <code>(shift | ctrl | alt)</code>
*  while dragging a box around an area of interest will zoom the map
*  to that area when the hot key is released. 
*  Only one line of code is needed: <code>google.maps.Map.enableKeyDragZoom();</code>
*  <p>
*  Note that if the map's container has a border around it, the border widths must be specified
*  in pixel units (or as thin, medium, or thick). This is required because of an MSIE limitation.
*  <p>NL: 2009-05-28: initial port to core API V3.
*/
/*!
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*       http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
    /*jslint browser:true */
    /*global google */
    /**
    * Converts 'thin', 'medium', and 'thick' to pixel widths
    * in an MSIE environment. Not called for other browsers
    * because getComputedStyle() returns pixel widths automatically.
    * @param {String} widthValue
    */
    var toPixels = function(widthValue) {
        var px;
        switch (widthValue) {
            case 'thin':
                px = "2px";
                break;
            case 'medium':
                px = "4px";
                break;
            case 'thick':
                px = "6px";
                break;
            default:
                px = widthValue;
        }
        return px;
    };
    /**
    * Get the widths of the borders of an HTML element.
    *
    * @param {Object} h  HTML element
    * @return {Object} widths object (top, bottom left, right)
    */
    var getBorderWidths = function(h) {
        var computedStyle;
        var bw = {};
        if (document.defaultView && document.defaultView.getComputedStyle) {
            computedStyle = h.ownerDocument.defaultView.getComputedStyle(h, "");
            if (computedStyle) {
                // The computed styles are always in pixel units (good!)
                bw.top = parseInt(computedStyle.borderTopWidth, 10) || 0;
                bw.bottom = parseInt(computedStyle.borderBottomWidth, 10) || 0;
                bw.left = parseInt(computedStyle.borderLeftWidth, 10) || 0;
                bw.right = parseInt(computedStyle.borderRightWidth, 10) || 0;
                return bw;
            }
        } else if (document.documentElement.currentStyle) { // MSIE
            if (h.currentStyle) {
                // The current styles may not be in pixel units so try to convert (bad!)
                bw.top = parseInt(toPixels(h.currentStyle.borderTopWidth), 10) || 0;
                bw.bottom = parseInt(toPixels(h.currentStyle.borderBottomWidth), 10) || 0;
                bw.left = parseInt(toPixels(h.currentStyle.borderLeftWidth), 10) || 0;
                bw.right = parseInt(toPixels(h.currentStyle.borderRightWidth), 10) || 0;
                return bw;
            }
        }
        // Shouldn't get this far for any modern browser
        bw.top = parseInt(h.style["border-top-width"], 10) || 0;
        bw.bottom = parseInt(h.style["border-bottom-width"], 10) || 0;
        bw.left = parseInt(h.style["border-left-width"], 10) || 0;
        bw.right = parseInt(h.style["border-right-width"], 10) || 0;
        return bw;
    };

    /**
    * Get the position of the mouse relative to the document.
    * @param {Object} e  Mouse event
    * @return {Object} left & top position
    */
    var getMousePosition = function(e) {
        var posX = 0, posY = 0;
        e = e || window.event;
        if (typeof e.pageX !== "undefined") {
            posX = e.pageX;
            posY = e.pageY;
        } else if (typeof e.clientX !== "undefined") {
            posX = e.clientX +
      (typeof document.documentElement.scrollLeft !== "undefined" ? document.documentElement.scrollLeft : document.body.scrollLeft);
            posY = e.clientY +
      (typeof document.documentElement.scrollTop !== "undefined" ? document.documentElement.scrollTop : document.body.scrollTop);
        }
        return {
            left: posX,
            top: posY
        };
    };

    /**
    * Get the position of an HTML element relative to the document.
    * @param {Object} h  HTML element
    * @return {Object} left & top position
    */
    var getElementPosition = function(h) {
        var posX = h.offsetLeft;
        var posY = h.offsetTop;
        var parent = h.offsetParent;
        // Add offsets for all ancestors in the hierarchy
        while (parent !== null) {
            // Adjust for scrolling elements which may affect the map position.
            //
            // See http://www.howtocreate.co.uk/tutorials/javascript/browserspecific
            //
            // "...make sure that every element [on a Web page] with an overflow
            // of anything other than visible also has a position style set to
            // something other than the default static..."
            if (parent !== document.body && parent !== document.documentElement) {
                posX -= parent.scrollLeft;
                posY -= parent.scrollTop;
            }
            posX += parent.offsetLeft;
            posY += parent.offsetTop;
            parent = parent.offsetParent;
        }
        return {
            left: posX,
            top: posY
        };
    };
    /**
    * Set the properties of an object to those from another object.
    * @param {Object} obj target object
    * @param {Object} vals source object
    */
    var setVals = function(obj, vals) {
        if (obj && vals) {
            for (var x in vals) {
                if (vals.hasOwnProperty(x)) {
                    obj[x] = vals[x];
                }
            }
        }
        return obj;
    };
    /**
    * Set the opacity. If op is not passed in, this function just performs an MSIE fix.
    * @param {Node} div
    * @param {Number} op (0-1)
    */
    var setOpacity = function(div, op) {
        if (typeof op !== 'undefined') {
            div.style.opacity = op;
        }
        if (typeof div.style.opacity !== 'undefined') {
            div.style.filter = "alpha(opacity=" + (div.style.opacity * 100) + ")";
        }
    };

    /**
    * @name KeyDragZoomOptions
    * @class This class represents the optional parameter passed into <code>google.maps.Map.enableDragBoxZoom</code>.
    * @property {String} [key] the hot key to hold down to activate a drag zoom, <code>shift | ctrl | alt</code>.
    * The default is <code>shift</code>.
    * @property {Object} [boxStyle] the css style of the zoom box.
    * The default is <code>{border: 'thin solid #FF0000'}</code>.
    * Border widths must be specified in pixel units (or as thin, medium, or thick).
    * @property {Object} [paneStyle] the css style of the pane which overlays the map when a drag zoom is activated.
    * The default is <code>{backgroundColor: 'white', opacity: 0.0, cursor: 'crosshair'}</code>.
    */
    /**
    * @name DragZoom
    * @class This class represents a drag zoom object for a map. The object is activated by holding down the hot key.
    * This object is created when <code>google.maps.Map.enableKeyDragZoom</code> is called; it cannot be created directly.
    * Use <code>google.maps.Map.getDragZoomObject</code> to gain access to this object in order to attach event listeners.
    * @param {google.maps.Map} map
    * @param {KeyDragZoomOptions} opt_zoomOpts
    */
    function DragZoom(map, opt_zoomOpts) {
        var ov = new google.maps.OverlayView();
        var me = this;
        ov.onAdd = function() {
            me.init_(map, opt_zoomOpts);
        };
        ov.draw = function() {
        };
        ov.onRemove = function() {
        };
        ov.setMap(map);
        this.prjov_ = ov;
    }
    /**
    * Init the tool. 
    * @param {google.maps.Map} map
    * @param {KeyDragZoomOptions} opt_zoomOpts
    */
    DragZoom.prototype.init_ = function(map, opt_zoomOpts) {
        this.map_ = map;
        opt_zoomOpts = opt_zoomOpts || {};
        this.key_ = opt_zoomOpts.key || 'shift';
        this.key_ = this.key_.toLowerCase();
        this.borderWidths_ = getBorderWidths(this.map_.getDiv()); //Container());
        this.paneDiv_ = document.createElement("div");
        this.paneDiv_.onselectstart = function() {
            return false;
        };
        // default style
        setVals(this.paneDiv_.style, {
            backgroundColor: 'white',
            opacity: 0.0,
            cursor: 'crosshair'
        });
        // allow overwrite 
        setVals(this.paneDiv_.style, opt_zoomOpts.paneStyle);
        // stuff that cannot be overwritten
        setVals(this.paneDiv_.style, {
            position: 'absolute',
            overflow: 'hidden',
            zIndex: 10001,
            display: 'none'
        });
        if (this.key_ === 'shift') { // Workaround for Firefox Shift-Click problem
            this.paneDiv_.style.MozUserSelect = "none";
        }
        setOpacity(this.paneDiv_);
        // An IE fix: if the background is transparent, it cannot capture mousedown events
        if (this.paneDiv_.style.backgroundColor === 'transparent') {
            this.paneDiv_.style.backgroundColor = 'white';
            setOpacity(this.paneDiv_, 0);
        }
        this.map_.getDiv().appendChild(this.paneDiv_); //Container()
        this.boxDiv_ = document.createElement('div');
        setVals(this.boxDiv_.style, {
            border: 'thin solid #FF0000'
        });
        setVals(this.boxDiv_.style, opt_zoomOpts.boxStyle);
        setVals(this.boxDiv_.style, {
            position: 'absolute',
            display: 'none'
        });
        setOpacity(this.boxDiv_);
        this.map_.getDiv().appendChild(this.boxDiv_);
        this.boxBorderWidths_ = getBorderWidths(this.boxDiv_);
        var me = this;
        this.keyDownListener_ = google.maps.event.addDomListener(document, 'keydown', function(e) {
            me.onKeyDown_(e);
        });
        this.keyUpListener_ = google.maps.event.addDomListener(document, 'keyup', function(e) {
            me.onKeyUp_(e);
        });
        this.mouseDownListener_ = google.maps.event.addDomListener(this.paneDiv_, 'mousedown', function(e) {
            me.onMouseDown_(e);
        });
        this.mouseDownListenerDocument_ = google.maps.event.addDomListener(document, 'mousedown', function(e) {
            me.onMouseDownDocument_(e);
        });
        this.mouseMoveListener_ = google.maps.event.addDomListener(document, 'mousemove', function(e) {
            me.onMouseMove_(e);
        });
        this.mouseUpListener_ = google.maps.event.addDomListener(document, 'mouseup', function(e) {
            me.onMouseUp_(e);
        });

        this.hotKeyDown_ = false;
        this.dragging_ = false;
        this.startPt_ = null;
        this.endPt_ = null;
        this.boxMaxX_ = null;
        this.boxMaxY_ = null;
        this.mousePosn_ = null;
        this.mapPosn_ = getElementPosition(this.map_.getDiv());
        this.mouseDown_ = false;
    };

    /**
    * Returns true if the hot key is being pressed when an event occurs.
    * @param {Event} e
    * @return {Boolean}
    */
    DragZoom.prototype.isHotKeyDown_ = function(e) {
        var isHot;
        e = e || window.event;
        isHot = (e.shiftKey && this.key_ === 'shift') || (e.altKey && this.key_ === 'alt') || (e.ctrlKey && this.key_ === 'ctrl');
        if (!isHot) {
            // Need to look at keyCode for Opera because it
            // doesn't set the shiftKey, altKey, ctrlKey properties
            // unless a non-modifier event is being reported.
            //
            // See http://cross-browser.com/x/examples/shift_mode.php
            // Also see http://unixpapa.com/js/key.html
            switch (e.keyCode) {
                case 16:
                    if (this.key_ === 'shift') {
                        isHot = true;
                    }
                    break;
                case 17:
                    if (this.key_ === 'ctrl') {
                        isHot = true;
                    }
                    break;
                case 18:
                    if (this.key_ === 'alt') {
                        isHot = true;
                    }
                    break;
            }
        }
        return isHot;
    };

    /**
    * Checks if the mouse is on top of the map. The position is captured 
    * in onMouseMove_.
    * @return true if mouse is on top of the map div.
    */
    DragZoom.prototype.isMouseOnMap_ = function() {
        var mousePos = this.mousePosn_;
        if (mousePos) {
            var mapPos = this.mapPosn_;
            var mapDiv = this.map_.getDiv();
            return mousePos.left > mapPos.left && mousePos.left < mapPos.left + mapDiv.offsetWidth &&
      mousePos.top > mapPos.top && mousePos.top < mapPos.top + mapDiv.offsetHeight;
        } else {
            // if user never moved mouse
            return false;
        }
    };

    /**
    * Show or hide the overlay pane, depending on whether the mouse is over the map.
    */
    DragZoom.prototype.setPaneVisibility_ = function() {
        if (this.map_ && this.hotKeyDown_ && this.isMouseOnMap_()) {
            var mapDiv = this.map_.getDiv();
            this.paneDiv_.style.left = 0 + 'px';
            this.paneDiv_.style.top = 0 + 'px';
            this.paneDiv_.style.width = mapDiv.offsetWidth - (this.borderWidths_.left + this.borderWidths_.right) + 'px';
            this.paneDiv_.style.height = mapDiv.offsetHeight - (this.borderWidths_.top + this.borderWidths_.bottom) + 'px';
            this.paneDiv_.style.display = 'block';
            this.boxMaxX_ = parseInt(this.paneDiv_.style.width, 10) - (this.boxBorderWidths_.left + this.boxBorderWidths_.right);
            this.boxMaxY_ = parseInt(this.paneDiv_.style.height, 10) - (this.boxBorderWidths_.top + this.boxBorderWidths_.bottom);
        } else {
            this.paneDiv_.style.display = 'none';
        }
    };
    /**
    * Handle key down. Activate the tool only if the mouse is on top of the map.
    * @param {Event} e
    */
    DragZoom.prototype.onKeyDown_ = function(e) {
        var me = this;
        if (this.map_ && !this.hotKeyDown_ && this.isHotKeyDown_(e)) {
            me.hotKeyDown_ = true;
            me.setPaneVisibility_();
            /**
            * This event is fired when the hot key is pressed. 
            * @name DragZoom#activate
            * @event
            */
            google.maps.event.trigger(me, 'activate');
        }
    };
    /**
    * Get the <code>google.maps.Point</code> of the mouse position.
    * @param {Object} e
    * @return {google.maps.Point} point
    * @private
    */
    DragZoom.prototype.getMousePoint_ = function(e) {
        var mousePosn = getMousePosition(e);
        var p = new google.maps.Point();
        p.x = mousePosn.left - this.mapPosn_.left - this.borderWidths_.left;
        p.y = mousePosn.top - this.mapPosn_.top - this.borderWidths_.top;
        p.x = Math.min(p.x, this.boxMaxX_);
        p.y = Math.min(p.y, this.boxMaxY_);
        p.x = Math.max(p.x, 0);
        p.y = Math.max(p.y, 0);
        return p;
    };
    /**
    * Handle mouse down.
    * @param {Event} e
    */
    DragZoom.prototype.onMouseDown_ = function(e) {
        if (this.map_ && this.hotKeyDown_) {
            this.mapPosn_ = getElementPosition(this.map_.getDiv());
            this.dragging_ = true;
            this.startPt_ = this.endPt_ = this.getMousePoint_(e);
            var prj = this.prjov_.getProjection();
            //var latlng = prj.fromDivPixelToLatLng(this.startPt_);
            var latlng = prj.fromContainerPixelToLatLng(this.startPt_);
            /**
            * This event is fired when the drag operation begins. 
            * @name DragZoom#dragstart
            * @param {GLatLng} startLatLng
            * @event
            */
            google.maps.event.trigger(this, 'dragstart', latlng);
        }
    };
    /**
    * Handle mouse down at the document level.
    * @param {Event} e
    */
    DragZoom.prototype.onMouseDownDocument_ = function(e) {
        this.mouseDown_ = true;
    };
    /**
    * Handle mouse move.
    * @param {Event} e
    */
    DragZoom.prototype.onMouseMove_ = function(e) {
        this.mousePosn_ = getMousePosition(e);
        if (this.dragging_) {
            this.endPt_ = this.getMousePoint_(e);
            var left = Math.min(this.startPt_.x, this.endPt_.x);
            var top = Math.min(this.startPt_.y, this.endPt_.y);
            var width = Math.abs(this.startPt_.x - this.endPt_.x);
            var height = Math.abs(this.startPt_.y - this.endPt_.y);
            this.boxDiv_.style.left = left + 'px';
            this.boxDiv_.style.top = top + 'px';
            this.boxDiv_.style.width = width + 'px';
            this.boxDiv_.style.height = height + 'px';
            this.boxDiv_.style.display = 'block';
            /**
            * This event is repeatedly fired while the user drags the box. The southwest and northeast
            * point are passed as parameters of type <code>google.maps.Point</code> (for performance reasons),
            * relative to the map container. Note: the event listener is responsible 
            * for converting Pixel to LatLng, if necessary.
            * @name DragZoom#drag 
            * @param {google.maps.Point} southwestPixel
            * @param {google.maps.Point} northeastPixel
            * @event
            */
            google.maps.event.trigger(this, 'drag', new google.maps.Point(left, top + height), new google.maps.Point(left + width, top));
        } else if (!this.mouseDown_) {
            this.setPaneVisibility_();
        }
    };
    /**
    * Handle mouse up.
    * @param {Event} e
    */
    DragZoom.prototype.onMouseUp_ = function(e) {
        this.mouseDown_ = false;
        if (this.dragging_) {
            var left = Math.min(this.startPt_.x, this.endPt_.x);
            var top = Math.min(this.startPt_.y, this.endPt_.y);
            var width = Math.abs(this.startPt_.x - this.endPt_.x);
            var height = Math.abs(this.startPt_.y - this.endPt_.y);
            var prj = this.prjov_.getProjection();
            // 2009-05-29: since V3 does not have fromContainerPixel, 
            //needs find offset here
            var containerPos = getElementPosition(this.map_.getDiv());
            var mapPanePos = getElementPosition(this.prjov_.getPanes().mapPane);
            left = left + (containerPos.left - mapPanePos.left);
            top = top + (containerPos.top - mapPanePos.top);
            var sw = prj.fromContainerPixelToLatLng(new google.maps.Point(left, top + height));
            var ne = prj.fromContainerPixelToLatLng(new google.maps.Point(left + width, top));
            var bnds = new google.maps.LatLngBounds(sw, ne);
            this.map_.fitBounds(bnds);
            this.dragging_ = false;
            this.boxDiv_.style.display = 'none';
            /**
            * This event is fired when the drag operation ends. 
            * Note that the event is not fired if the hot key is released before the drag operation ends.
            * @name DragZoom#dragend
            * @param {GLatLngBounds} newBounds
            * @event
            */
            google.maps.event.trigger(this, 'dragend', bnds);
        }
    };

    /**
    * Handle key up.
    * @param {Event} e
    */
    DragZoom.prototype.onKeyUp_ = function(e) {
        if (this.map_ && this.hotKeyDown_) {
            this.hotKeyDown_ = false;
            this.dragging_ = false;
            this.boxDiv_.style.display = 'none';
            this.paneDiv_.style.display = "none";
            /**
            * This event is fired while the user release the key
            * @name DragZoom#deactivate 
            * @event
            */
            google.maps.event.trigger(this, 'deactivate');
        }
    };




    /**
    * @name google.maps.Map
    * @class These are new methods added to the Google Maps API's
    * <a href  = 'http://code.google.com/apis/maps/documentation/v3/reference.html#Map'>Map</a>
    * class.
    */
    /**
    * Enable drag zoom. The user can zoom to an area of interest by holding down the hot key
    * <code>(shift | ctrl | alt )</code> while dragging a box around the area. 
    * @param {KeyDragZoomOptions} opt_zoomOpts
    */

    google.maps.Map.prototype.enableKeyDragZoom = function(opt_zoomOpts) {
        this.dragZoom_ = new DragZoom(this, opt_zoomOpts);
    };
    /**
    * Disable drag zoom.
    */
    google.maps.Map.prototype.disableKeyDragZoom = function() {
        var d = this.dragZoom_;
        if (d) {
            google.maps.event.removeListener(d.mouseDownListener_);
            google.maps.event.removeListener(d.mouseDownListenerDocument_);
            google.maps.event.removeListener(d.mouseMoveListener_);
            google.maps.event.removeListener(d.mouseUpListener_);
            google.maps.event.removeListener(d.keyUpListener_);
            google.maps.event.removeListener(d.keyDownListener_);
            this.getDiv().removeChild(d.boxDiv_);
            this.getDiv().removeChild(d.paneDiv_);
            this.dragZoom_ = null;
        }
    };
    /**
    * Returns true if the drag zoom feature has been enabled.
    * @return {Boolean}
    */
    google.maps.Map.prototype.keyDragZoomEnabled = function() {
        return this.dragZoom_ !== null;
    };
    /**
    * Returns the DragZoom object which is created when <code>google.maps.Map.enableKeyDragZoom</code> is called.
    * With this object you can use <code>google.maps.event.addListener</code> to attach event listeners
    * for the 'activate', 'deactivate', 'dragstart', 'drag', and 'dragend' events.
    * @return {DragZoom}
    */
    google.maps.Map.prototype.getDragZoomObject = function() {
        return this.dragZoom_;
    };
})();
