// ----------------------------------------------------------------------------
// Copyright 2007-2013, GeoTelematic Solutions, Inc.
// All rights reserved
// ----------------------------------------------------------------------------
//
// 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.
//
// ----------------------------------------------------------------------------
// Required funtions defined by this module:
//   new JSMap(String mapID)
//   JSClearLayers()
//   JSSetCenter(JSMapPoint center [, int zoom])
//   JSDrawPushpins(JSMapPushpin pushPin[], int recenterMode, int replay)
//   JSDrawRoute(JSMapPoint points[], String color)
//   JSDrawShape(String type, double radius, JSMapPoint points[], String color, boolean zoomTo)
//   JSDrawGeozone(int type, double radius, JSMapPoint points[], String color, int primaryIndex)
//   JSShowPushpin(JSMapPushpin pushPin, boolean center)
//   JSUnload()
// ----------------------------------------------------------------------------
// Change History:
//  2012/04/20  Martin D. Flynn
//     -Initial release (cloned from VirtualEarth.js)
// ----------------------------------------------------------------------------

var VEPushpin_XOfs      = 13;
var VEPushpin_YOfs      = 13;

var DRAG_NONE           = 0x00;
var DRAG_RULER          = 0x01;
var DRAG_GEOZONE        = 0x10;
var DRAG_GEOZONE_CENTER = 0x11;
var DRAG_GEOZONE_RADIUS = 0x12;

/**
*** JSMap constructor
**/
function JSMap(element)
{

    /* map */
    try {
        this.bingMap = new Microsoft.Maps.Map(element, null);
        this.virtualEarthMap = this.bingMap;
    } catch (e) {
        alert("Error loading Bing map:\n" + e);
        if (this.bingMap == null) { return; }
    }

    /* map style */
    this._setDefaultMapStyle();

    /* map attributes */
    this.virtEarthMap.SetMouseWheelZoomToCenter(SCROLL_WHEEL_ZOOM);
    element.style.cursor = "crosshair";

    /* last mousedown X/Y */
    this.lastX = 0;
    this.lastY = 0;

    /* draw layers */
    this.virtEarthGeozoneLayer = this.virtEarthMap;
    this.virtEarthShapeLayer = new VEShapeLayer();
    this.virtEarthMap.AddShapeLayer(this.virtEarthShapeLayer);
    this.virtEarthRulerLayer = new VEShapeLayer();
    this.virtEarthMap.AddShapeLayer(this.virtEarthRulerLayer);
    
    /* popup info box */
    this.visiblePopupInfoBox = null;
    
    /* drawn shapes */
    this.drawShapes = [];

    /* replay vars */
    this.replayTimer = null;
    this.replayIndex = 0;
    this.replayInterval = (REPLAY_INTERVAL < 100)? 100 : REPLAY_INTERVAL;
    this.replayInProgress = false;
    this.replayPushpins = [];

    /* drag vars */
    this.dragType = DRAG_NONE;
    this.dragRulerLatLon = null;
    this.dragMarker = null;
    this.dragZoneOffsetLat = 0.0;
    this.dragZoneOffsetLon = 0.0;
    this.geozoneCenter = null;
    this.geozoneShape  = null;
    this.geozonePoints = null;      // JSMapPoint[]
    this.primaryIndex  = -1;
    this.primaryCenter = null;      // JSMapPoint

    /* Lat/Lon display */
    this.latLonDisplay = jsmGetLatLonDisplayElement();
    jsmSetLatLonDisplay(0,0);

    /* mouse event handlers */
    try {
        var self = this;
        this.virtEarthMap.AttachEvent("onmousedown" , function (e) { return self._event_OnMouseDown(e); });
        this.virtEarthMap.AttachEvent("onmousemove" , function (e) { return self._event_OnMouseMove(e); });
        this.virtEarthMap.AttachEvent("onmouseup"   , function (e) { return self._event_OnMouseUp(e);   });
        this.virtEarthMap.AttachEvent("onmouseover" , function (e) { return self._event_OnMouseOver(e); });
        this.virtEarthMap.AttachEvent("onmouseout"  , function (e) { return self._event_OnMouseOut(e);  });
        this.virtEarthMap.AttachEvent("onclick"     , function (e) { return self._event_OnClick(e);     });
    } catch (e) {
        alert("Error setting mouse events:\n" + e);
    }

};

/**
*** Set map style
**/
JSMap.prototype._setDefaultMapStyle = function()
{
    if ((DEFAULT_VIEW == "aerial") || (DEFAULT_VIEW == "satellite")) {
        this.bingMap.setView({ mapTypeId : Microsoft.Maps.MapTypeId.aerial });
    } else
    if ((DEFAULT_VIEW == "hybrid") || (DEFAULT_VIEW == "auto")) {
        this.bingMap.setView({ mapTypeId : Microsoft.Maps.MapTypeId.auto });
    } else
    if (DEFAULT_VIEW == "birdseye") {
        this.bingMap.setView({ mapTypeId : Microsoft.Maps.MapTypeId.birdseye });
    } else {
        this.bingMap.setView({ mapTypeId : Microsoft.Maps.MapTypeId.road });
    }
};

// ----------------------------------------------------------------------------

/**
*** Unload/release resources
**/
JSMap.prototype.JSUnload = function()
{
    //
};

// ----------------------------------------------------------------------------

/**
*** Clear all pushpins and drawn lines
**/
JSMap.prototype.JSClearLayers = function()
{
    try { this.virtEarthMap.DeleteAllShapes();        } catch (e) {}
    try { this.virtEarthMap.DeleteAllPolylines();     } catch (e) {}
    this._removeShapes();
    this._clearReplay();
    this.centerBoundsPoints = [];
};

// ----------------------------------------------------------------------------

/**
*** Pause/Resume replay
**/
JSMap.prototype.JSPauseReplay = function(replay)
{
    /* stop replay? */
    if (!replay || (replay <= 0) || !this.replayInProgress) {
        // stopping replay
        this._clearReplay();
        return REPLAY_STOPPED;
    } else {
        // replay currently in progress
        if (this.replayTimer == null) {
            // replay is "PAUSED" ... resuming replay
            this.virtEarthMap.HideInfoBox(); // hide any displayed InfoBox
            jsmHighlightDetailRow(-1, false);
            this._startReplayTimer(replay, 100);
            return REPLAY_RUNNING;
        } else {
            // replaying "RUNNING" ... pausing replay
            this._stopReplayTimer();
            return REPLAY_PAUSED;
        }
    }
};

/**
*** Start the replay timer
**/
JSMap.prototype._startReplayTimer = function(replay, interval)
{
    if (this.replayInProgress) {
        this.replayTimer = setTimeout("jsmap._replayPushpins("+replay+")", interval);
    }
    jsmSetReplayState(REPLAY_RUNNING);
};

/**
*** Stop the current replay timer
**/
JSMap.prototype._stopReplayTimer = function()
{
    if (this.replayTimer != null) { 
        clearTimeout(this.replayTimer); 
        this.replayTimer = null;
    }
    jsmSetReplayState(this.replayInProgress? REPLAY_PAUSED : REPLAY_STOPPED);
};

/**
*** Clear any current replay in process
**/
JSMap.prototype._clearReplay = function()
{
    this.replayPushpins = [];
    this.replayInProgress = false;
    this._stopReplayTimer();
    this.replayIndex = 0;
    jsmHighlightDetailRow(-1, false);
};

/**
*** Gets the current replay state
**/
JSMap.prototype._getReplayState = function()
{
    if (this.replayInProgress) {
        if (this.replayTimer == null) {
            return REPLAY_PAUSED;
        } else {
            return REPLAY_RUNNING;
        }
    } else {
        return REPLAY_STOPPED;
    }
};

// ----------------------------------------------------------------------------

/**
*** Sets the center of the map
**/
JSMap.prototype.JSSetCenter = function(center, zoom)
{
    var vpt = new VELatLong(center.lat, center.lon);
    // I'd prefer that this "recentering" NOT be animated!
    var animate = this.virtEarthMap.vemapcontrol.IsAnimationEnabled();
    this.virtEarthMap.vemapcontrol.SetAnimationEnabled(false);
    if (!zoom || (zoom == 0)) {
        this.virtEarthMap.SetCenter(vpt);
    } else
    if (zoom > 0) {
        this.virtEarthMap.SetCenterAndZoom(vpt, zoom);
    } else {
        var vb = new JSBounds();
        vb.extend(center);
        zoom = this._calcBestZoom(vb);
        //alert("Zoom = " + zoom);
        this.virtEarthMap.SetCenterAndZoom(vpt, zoom);
    }
    this.virtEarthMap.vemapcontrol.SetAnimationEnabled(animate);
};

/**
*** Draw the specified pushpins on the map
*** @param pushPins  An array of JSMapPushpin objects
*** @param recenter  True to cause the map to re-center on the drawn pushpins
*** @param replay    Replay mode
**/
JSMap.prototype.JSDrawPushpins = function(pushPins, recenterMode, replay)
{

    /* clear replay (may be redundant, but repeated just to make sure) */
    this._clearReplay();
    this.virtEarthMap.HideInfoBox(); // hide any displayed InfoBox
    var mapStyle = this.bingMap.getImageryId();

    /* drawn pushpins */
    var drawPushpins = [];

    /* recenter map on points */
    var points = [];
    var pointCount = 0;
    if ((pushPins != null) && (pushPins.length > 0)) {
        // extend bounding box around displayed pushpins
        for (var i = 0; i < pushPins.length; i++) {
            var pp = pushPins[i]; // JSMapPushpin
            if (pp.show && ((pp.lat != 0.0) || (pp.lon != 0.0))) {
                pointCount++;
                var veLatLon = new VELatLong(pp.lat,pp.lon);
                this.centerBoundsPoints.push(veLatLon);
                points.push(veLatLon);
                drawPushpins.push(pp);
            }
        }
    }
    if (recenterMode > 0) {
        // Recenter modes:
        //  0 = none (leave map positioned as-is)
        //  1 = center on last point only (no zoom)
        //  2 = center and zoom
        try {
            if (pointCount <= 0) {
                var centerPt   = new VELatLong(DEFAULT_CENTER.lat, DEFAULT_CENTER.lon);
                var zoomFactor = DEFAULT_ZOOM;
                this._setCenter(centerPt, zoomFactor);
            } else
            if (recenterMode == RECENTER_LAST) { // center on last point
                var centerPt   = points[points.length - 1];
                var zoomFactor = -1; // default VirtualEarth zoom
                this._setCenter(centerPt, zoomFactor);
            } else
            if (recenterMode == RECENTER_PAN) { // pan to last point
                var centerPt   = points[points.length - 1];
                var zoomFactor = -1; // default VirtualEarth zoom
                this._setCenter(centerPt, zoomFactor);
            } else {
                if (mapStyle == VEMapStyle.Birdseye) {
                    this._setDefaultMapStyle();
                }
                this.virtEarthMap.SetMapView(this.centerBoundsPoints);
            }
        } catch (e) {
            //alert("Error: [JSDrawPushpins] " + e);
            return;
        }
    }
    if (pointCount <= 0) {
        return;
    }

    /* replay pushpins? */
    if (replay && (replay >= 1)) {
        this.replayIndex = 0;
        this.replayInProgress = true;
        this.replayPushpins = drawPushpins;
        this._startReplayTimer(replay, 100);
        return;
    }

    /* draw pushpins now */
    var pushpinErr = null;
    for (var i = 0; i < drawPushpins.length; i++) {
        var pp = drawPushpins[i];
        try {
            this._addPushpin(pp);
        } catch (e) {
            if (pushpinErr == null) { pushpinErr = e; }
        }
    }

    /* Birdseye pushpin accuracy */
    //this.virtEarthMap.SetShapesAccuracy(VEShapeAccuracy.Pushpin);

    /* Birdseye scene */
    //var lastPP = drawPushpins[drawPushpins.length - 1];
    //this.virtEarthMap.SetBirdseyeScene(new VELatLong(lastPP.lat, lastPP.lon));

    /* any errors? */
    if (pushpinErr != null) {
        alert("Error: adding pushpins:\n" + pushpinErr);
    }

};

/**
*** Draw the specified PointsOfInterest pushpins on the map
*** @param pushPins  An array of JSMapPushpin objects
**/
JSMap.prototype.JSDrawPOI = function(pushPins)
{
    
    /* hide infobox */
    this.virtEarthMap.HideInfoBox(); // hide any displayed InfoBox
    
    /* draw pushpins now */
    if ((pushPins != null) && (pushPins.length > 0)) {
        var pushpinErr = null;
        for (var i = 0; i < pushPins.length; i++) {
            var pp = pushPins[i];
            try {
                this._addPushpin(pp);
            } catch (e) {
                if (pushpinErr == null) { pushpinErr = e; }
            }
        }
    }

    /* any errors? */
    if (pushpinErr != null) {
        alert("Error: adding pushpins:\n" + pushpinErr);
    }

};

/**
*** Sets the center/zoom of the map
**/
JSMap.prototype._setCenter = function(center, zoom)
{
    if (zoom && (zoom > 0)) {
        this.virtEarthMap.SetCenterAndZoom(center, zoom);
    } else {
        this.virtEarthMap.SetCenter(center);
    }
};

/**
*** Adds a single pushpin to the map
*** @param pp  The JSMapPushpin object to add to the map
**/
JSMap.prototype._addPushpin = function(pp) // JSMapPushpin
{

    pp.map = this.virtEarthMap;

    /* background marker */
    if (pp.bgUrl) {
        var bgMarker = new VEShape(VEShapeType.Pushpin, new VELatLong(pp.lat, pp.lon));
        bgMarker.SetTitle(" ");
        bgMarker.SetDescription("");
        bgMarker.pp = pp;
        var bgURL  = pp.bgUrl;
        var bgXOfs = VEPushpin_XOfs - pp.bgOffset[0];
        var bgYOfs = VEPushpin_YOfs - pp.bgOffset[1];
        var bgHTML = "<div style='position:relative; top:"+bgYOfs+"px; left:"+bgXOfs+"px'><img src='"+bgURL+"'></div>";
        bgMarker.SetCustomIcon(bgHTML);
        this.virtEarthMap.AddShape(bgMarker);
        pp.bgMarker = bgMarker;
    }

    /* pushpin marker */
    // Offset to hotspot
    var ppMarker = new VEShape(VEShapeType.Pushpin, new VELatLong(pp.lat, pp.lon));
    //ppMarker.SetTitle('<small>[#' + ndx + '] <b>' + dev + ' : ' + code + '</b></small>');",
    // http://msdn.microsoft.com/en-us/library/bb412441.aspx
    ppMarker.SetTitle(" ");
    ppMarker.SetDescription(pp.getHTML());
    ppMarker.pp = pp;
    // Using 'margins' here positions the push-pin properly, but may also cause IE to clip the image
    // var imgURL = "<div style='margin-left:"+ppXOfs+"px; margin-top:"+ppYOfs+"px;'><img src='"+ppURL+"'></div>";
    var ppURL  = pp.iconUrl;
    var ppXOfs = VEPushpin_XOfs - pp.iconHotspot[0];
    var ppYOfs = VEPushpin_YOfs - pp.iconHotspot[1];
    var ppHTML = "<div style='position:relative; top:"+ppYOfs+"px; left:"+ppXOfs+"px'><img src='"+ppURL+"'></div>";
    ppMarker.SetCustomIcon(ppHTML);
    // var iconSpec = new VECustomIconSpecification(null,imgURL,'Test',null,false,true,false,10,null,null,null,null);
    // ppMarker.SetCustomIcon(iconSpec);  // 3D
    // TODO: "mouseover" events?

    /* marker */
    this.virtEarthMap.AddShape(ppMarker);
    pp.marker = ppMarker;

};

/**
*** Replays the list of pushpins on the map
*** @param pp  The JSMapPushpin object to add to the map
**/
JSMap.prototype._replayPushpins = function(replay)
{

    /* advance to next valid point */
    while (true) {
        if (this.replayIndex >= this.replayPushpins.length) {
            this._clearReplay();
            jsmHighlightDetailRow(-1, false);
            return; // stop
        }
        var pp = this.replayPushpins[this.replayIndex]; // JSMapPushpin
        if ((pp.lat != 0.0) || (pp.lon != 0.0)) {
            break; // valid point
        }
        this.replayIndex++;
    }

    /* add pushpin */
    try {
        var lastNdx = this.replayIndex - 1;
        var pp = this.replayPushpins[this.replayIndex++]; // JSMapPushpin
        pp.hoverPopup = true;
        if (REPLAY_SINGLE && (lastNdx >= 0)) {
            var lastPP = this.replayPushpins[lastNdx]; // JSMapPushpin
            if (lastPP.marker) {
                this.virtEarthMap.DeleteShape(lastPP.marker);
            }
            if (lastPP.bgMarker) {
                this.virtEarthMap.DeleteShape(lastPP.bgMarker);
            }
        }
        this._addPushpin(pp);
        if (replay && (replay >= 2)) {
            this._showPushpinPopup(pp);
        } else {
            jsmHighlightDetailRow(pp.rcdNdx, true);
        }
        this._startReplayTimer(replay, this.replayInterval);
    } catch (e) {
        // ignore
    }

};

// ----------------------------------------------------------------------------

/**
*** This method should cause the info-bubble popup for the specified pushpin to display
*** @param pushPin   The JSMapPushpin object which popup its info-bubble
**/
JSMap.prototype.JSShowPushpin = function(pp, center)
{
    if (pp) {
        if (pp.popupShown) {
            this._hidePushpinPopup(pp);
        } else {
            if (center || !this._isPointOnMap(pp.lat,pp.lon,5,5,5,5)) {
                this.JSSetCenter(new JSMapPoint(pp.lat, pp.lon));
            }
            this._showPushpinPopup(pp);
        }
    }
};

JSMap.prototype._isPointOnMap = function(lat, lon, margTop, margLeft, margBott, margRght)
{
    if ((MAP_HEIGHT > 0) && (MAP_WIDTH > 0)) {
        var top   = 0                 + margTop; // this.virtEarthMap.GetTop();
        var left  = 0                 + margLeft; // this.virtEarthMap.GetLeft();
        var bott  = top  + MAP_HEIGHT - margBott;
        var rght  = left + MAP_WIDTH  - margRght;
        var TL    = this.virtEarthMap.PixelToLatLong(new VEPixel(left, top ));
        var BR    = this.virtEarthMap.PixelToLatLong(new VEPixel(rght, bott));
        //alert("TopLeft="+TL.Latitude+"/"+TL.Longitude+", BottomRight="+BR.Latitude+"/"+BR.Longitude);
        if ((lat > TL.Latitude) || (lat < BR.Latitude)) {
            return false;
        } else
        if ((lon < TL.Longitude) || (lon > BR.Longitude)) {
            return false;
        } else {
            return true;
        }
    } else {
        return true;
    }
};

JSMap.prototype._showPushpinPopup = function(pp)
{
    this._hidePushpinPopup(this.visiblePopupInfoBox);
    if (pp && !pp.popupShown && pp.map) {
        pp.map.ShowInfoBox(pp.marker);
        pp.popupShown = true;
        this.visiblePopupInfoBox = pp;
        jsmHighlightDetailRow(pp.rcdNdx, true);
    } else {
        this.visiblePopupInfoBox = null;
    }
};

JSMap.prototype._hidePushpinPopup = function(pp)
{
    if (pp && pp.popupShown) {
        pp.map.HideInfoBox(pp.marker);
        pp.popupShown = false;
        jsmHighlightDetailRow(pp.rcdNdx, false);
    }
};

// ----------------------------------------------------------------------------

/**
*** Draws a line between the specified points on the map.
*** @param points   An array of JSMapPoint objects
**/
var routLineNdx = 0;
JSMap.prototype.JSDrawRoute = function(points, color)
{
    if (points && (points.length >= 2)) {
        var latlon = [];
        for (var i = 0; i < points.length; i++) {
            latlon.push(new VELatLong(points[i].lat, points[i].lon));
        }
        try {
            var name = "routeLine" + (routLineNdx++);
            var rgb  = rgbVal(color); // ie. Convert "#FF2222" to { R:255, G:34, B:34 }
            var veColor = new VEColor(rgb.R,rgb.G,rgb.B,1);
            this.virtEarthMap.AddPolyline(new VEPolyline(name, latlon, veColor, 2));
        } catch (e) {
            alert("Error creating route:\n" + e);
        }
    }
};

// ----------------------------------------------------------------------------

/**
*** Remove previously drawn shapes 
**/
JSMap.prototype._removeShapes = function()
{
    this.virtEarthShapeLayer.DeleteAllShapes();
    this.drawShapes = [];
};

/**
*** Draws a Shape on the map at the specified location
*** @param type     The Geozone shape type
*** @param radiusM  The circle radius, in meters
*** @param points   An array of points (JSMapPoint[])
*** @param color    shape color
*** @param zoomTo   rue to zoom to drawn shape
*** @return True if shape was drawn, false otherwise
**/
JSMap.prototype.JSDrawShape = function(type, radiusM, verticePts, color, zoomTo)
{

    /* no type? */
    if (!type || (type == "") || (type == "!")) {
        this._removeShapes();
        return false;
    }

    /* clear existing shapes? */
    if (type.startsWith("!")) { 
        this._removeShapes();
        type = type.substr(1); 
    }

    /* no geopoints? */
    if (!verticePts || (verticePts.length == 0)) {
        return false;
    }
    
    /* color */
    if (!color || (color == "")) {
        color = "#0000FF";
    }
    var rgb = rgbVal(color);  // ie. Convert "#FF2222" to { R:255, G:34, B:34 }

    /* zoom bounds */
    var mapBounds = zoomTo? new JSBounds() : null;

    /* draw shape */
    var didDrawShape = false;
    if (type == "circle") { // ZONE_POINT_RADIUS

        for (var i = 0; i < verticePts.length; i++) {
            var vp = verticePts[i]; // JSMapPoint
            if ((vp.lat == 0.0) && (vp.lon == 0.0)) { continue; }
            var center = new VELatLong(vp.lat,vp.lon);
            var circlePoints = this._getCirclePoints(center, radiusM);
            var shape = new VEShape(VEShapeType.Polygon, circlePoints);
            shape.SetTitle('<small>Circle</small>');
            shape.SetLineWidth(1);
            shape.SetDescription("Circle");
            shape.HideIcon();  // remove the pushpin at the center of the circle
            shape.SetLineColor(new VEColor(rgb.R,rgb.G,rgb.B,1.0));
            shape.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B,0.1));
            if (mapBounds) { 
                mapBounds.extend(vp);
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM,   0.0));
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM,  90.0));
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM, 180.0));
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM, 270.0));
            }
            this.drawShapes.push(shape);
            this.virtEarthShapeLayer.AddShape(shape);
            didDrawShape = true;
        }

    } else
    if (type == "rectangle") {

        if (verticePts.length >= 2) {

            /* create rectangle */
            var vp0   = verticePts[0];
            var vp1   = verticePts[1];
            var TL    = new VELatLong(((vp0.lat>vp1.lat)?vp0.lat:vp1.lat),((vp0.lon<vp1.lon)?vp0.lon:vp1.lon));
            var TR    = new VELatLong(((vp0.lat>vp1.lat)?vp0.lat:vp1.lat),((vp0.lon>vp1.lon)?vp0.lon:vp1.lon));
            var BL    = new VELatLong(((vp0.lat<vp1.lat)?vp0.lat:vp1.lat),((vp0.lon<vp1.lon)?vp0.lon:vp1.lon));
            var BR    = new VELatLong(((vp0.lat<vp1.lat)?vp0.lat:vp1.lat),((vp0.lon>vp1.lon)?vp0.lon:vp1.lon));
            var vePts = [ TL, TR, BR, BL, TL ];
            var shape = new VEShape(VEShapeType.Polygon, vePts);
            shape.SetTitle("<small>Rectangle</small>");
            shape.SetLineWidth(1);
            shape.SetDescription("Rectangle");
            shape.HideIcon();  // remove the pushpin at the center of the circle
            shape.SetLineColor(new VEColor(rgb.R,rgb.G,rgb.B,1.0));
            shape.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B,0.1));
            if (mapBounds) {
                mapBounds.extend(vp0); 
                mapBounds.extend(vp1); 
            }
            this.drawShapes.push(shape);
            this.virtEarthShapeLayer.AddShape(shape);
            didDrawShape = true;

        }
            
    } else
    if (type == "polygon") {
       
        if (verticePts.length >= 3) {

            var vePts = [];
            for (var p = 0; p < verticePts.length; p++) {
                var vePt = new VELatLong(verticePts[p].lat, verticePts[p].lon);
                vePts.push(vePt);
                if (mapBounds) { 
                    mapBounds.extend(verticePts[p]); 
                }
            }
            var poly = new VEShape(VEShapeType.Polygon, vePts);
            poly.SetTitle("<small>Polygon</small>");
            poly.SetLineWidth(1);
            poly.SetDescription("Rectangle");
            poly.HideIcon();  // remove the pushpin at the center of the circle
            poly.SetLineColor(new VEColor(rgb.R,rgb.G,rgb.B,1.0));
            poly.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B,0.1));
            this.drawShapes.push(poly);
            this.virtEarthShapeLayer.AddShape(poly);
            didDrawShape = true;

        }

    } else
    if (type == "corridor") { // ZONE_SWEPT_POINT_RADIUS

        var lastPT = null;
        for (var i = 0; i < verticePts.length; i++) {
            var vp = verticePts[i]; // JSMapPoint
            if ((vp.lat == 0.0) && (vp.lon == 0.0)) { continue; }
            var center = new VELatLong(vp.lat,vp.lon);
            var circlePoints = this._getCirclePoints(center, radiusM);
            var circleShape = new VEShape(VEShapeType.Polygon, circlePoints);
            circleShape.SetTitle('<small>Corridor</small>');
            circleShape.SetLineWidth(1);
            circleShape.SetDescription("Corridor");
            circleShape.HideIcon();  // remove the pushpin at the center of the circle
            circleShape.SetLineColor(new VEColor(rgb.R,rgb.G,rgb.B,1.0));
            circleShape.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B,0.1));
            if (mapBounds) { 
                mapBounds.extend(vp);
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM,   0.0));
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM,  90.0));
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM, 180.0));
                mapBounds.extend(this._calcRadiusPoint(vp, radiusM, 270.0));
            }
            this.drawShapes.push(circleShape);
            this.virtEarthShapeLayer.AddShape(circleShape);
            
            if (lastPT != null) {
                var ptA = lastPT;   // JSMapPoint
                var ptB = vp;       // JSMapPoint
                var hAB = geoHeading(ptA.lat, ptA.lon, ptB.lat, ptB.lon) - 90.0; // perpendicular
                var rp1 = this._calcRadiusPoint(ptA, radiusM, hAB        ); // JSMapPoint
                var rp2 = this._calcRadiusPoint(ptB, radiusM, hAB        ); // JSMapPoint
                var rp3 = this._calcRadiusPoint(ptB, radiusM, hAB + 180.0); // JSMapPoint
                var rp4 = this._calcRadiusPoint(ptA, radiusM, hAB + 180.0); // JSMapPoint
                var rectPts = [];
                rectPts.push(new VELatLong(rp1.lat, rp1.lon));
                rectPts.push(new VELatLong(rp1.lat, rp1.lon));
                rectPts.push(new VELatLong(rp1.lat, rp1.lon));
                rectPts.push(new VELatLong(rp1.lat, rp1.lon));
                var rectShape = new VEShape(VEShapeType.Polygon, rectPts);
                rectShape.SetTitle("<small>Corridor</small>");
                rectShape.SetLineWidth(1);
                rectShape.SetDescription("Corridor");
                rectShape.HideIcon();  // remove the pushpin at the center of the circle
                rectShape.SetLineColor(new VEColor(rgb.R,rgb.G,rgb.B,1.0));
                rectShape.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B,0.1));
                this.drawShapes.push(rectShape);
                this.virtEarthShapeLayer.AddShape(rectShape);
            }
            lastPT = vp;
            
            didDrawShape = true;
        }

    } else
    if (type == "center") {

        if (mapBounds) {
            for (var p = 0; p < verticePts.length; p++) {
                var jsPt = new JSMapPoint(verticePts[p].lat, verticePts[p].lon);
                mapBounds.extend(jsPt);
            }
            didDrawShape = true;
        }

    }

    /* center on shape */
    if (didDrawShape && zoomTo && mapBounds) {
        var centerPt   = mapBounds.getCenter(); // JSMapPoint
        var zoomFactor = this._calcBestZoom(mapBounds);
        this._setCenter(new VELatLong(centerPt.lat, centerPt.lon), zoomFactor);
    }

    /* shape not supported */
    return didDrawShape;

};

// ----------------------------------------------------------------------------

/**
*** Draws a Geozone on the map at the specified location
*** @param type     The Geozone type
*** @param editable True to allow editing the circle (position, radius)
*** @param radiusM  The circle radius, in meters
*** @param points   An array of points
*** @param primNdx  Index of primary point
*** @return An object representing the Circle.
**/
JSMap.prototype.JSDrawGeozone = function(type, radiusM, points, color, primNdx)
{
    // only type 0 (pointRadius) is currently supported

    /* Geozone mode */
    jsvGeozoneMode = true;

    /* remove old geozone */
    //for (var i = 0; i < this.geozonePoints.length; i++) { this.geozonePoints[i].remove(); }
    //this.geozonePoints = [];
    this.geozoneShape = null;
    this.geozoneCenter = null;
    this.virtEarthGeozoneLayer.DeleteAllShapes();
    //this.virtEarthGeozoneTempLayer.DeleteAllShapes();

    /* save geozone points */
    this.geozonePoints = points;
    this.primaryIndex  = primNdx;

    /* no points? */
    if ((points == null) || (points.length <= 0)) {
        //alert("No Zone center!");
        return null;
    }
    
    /* zone shape color */
    this.shapeColor = (color && (color != ""))? color : "#00B400";
    this.lineWidth  = 3;
    //this.opacity    = 1.0;

    /* point-radius */
    //type = ZONE_POLYGON;
    if (type == ZONE_POINT_RADIUS) {

        /* adjust radius */
        if (isNaN(radiusM))              { radiusM = 5000; }
        if (radiusM > MAX_ZONE_RADIUS_M) { radiusM = MAX_ZONE_RADIUS_M; }
        if (radiusM < MIN_ZONE_RADIUS_M) { radiusM = MIN_ZONE_RADIUS_M; }

        /* draw points */
        var count = 0;
        var mapBounds = new JSBounds();
        for (var i = 0; i < points.length; i++) {
            var c = points[i]; // JSMapPoint
            if ((c.lat != 0.0) || (c.lon != 0.0)) {
                var isPrimary = (i == primNdx);
                if (jsvZoneEditable || jsvShowVertices) {
                    this._addCircleShape(new VELatLong(c.lat,c.lon), radiusM, this.shapeColor, this.lineWidth, isPrimary);
                }
                mapBounds.extend(c);
                mapBounds.extend(this._calcRadiusPoint(c, radiusM,   0.0));
                mapBounds.extend(this._calcRadiusPoint(c, radiusM,  90.0));
                mapBounds.extend(this._calcRadiusPoint(c, radiusM, 180.0));
                mapBounds.extend(this._calcRadiusPoint(c, radiusM, 270.0));
                count++;
            }
        }

        /* center on geozone */
        var centerPt = DEFAULT_CENTER; // JSMapPoint
        var zoom     = DEFAULT_ZOOM;
        if (count > 0) {
            centerPt = mapBounds.getCenter(); // JSMapPoint
            zoom     = this._calcBestZoom(mapBounds);
        }
        this._setCenter(new VELatLong(centerPt.lat, centerPt.lon), zoom);

    } else
    if (type == ZONE_POLYGON) {

        /* adjust radius */
        //if (isNaN(radiusM))              { radiusM = 5000; }
        //if (radiusM > MAX_ZONE_RADIUS_M) { radiusM = MAX_ZONE_RADIUS_M; }
        //if (radiusM < MIN_ZONE_RADIUS_M) { radiusM = MIN_ZONE_RADIUS_M; }

        var isDragging = false;

        /* draw points */
        var count = 0;
        var mapBounds = new JSBounds();
        var polyPts = []; // JSMapPoint[]
        var polyPtPrim = -1;
        for (var i = 0; i < points.length; i++) {
            var c = points[i]; // JSMapPoint
            if ((c.lat != 0.0) || (c.lon != 0.0)) {
                var isPrimary = (i == primNdx);
                var center    = (isPrimary && isDragging)? this.primaryCenter : c;
                if (isPrimary) {
                    this.primaryCenter = center; // OpenLayers.LonLat
                    polyPtsPrim = polyPts.length;
                }
                polyPts.push(center); // JSMapPoint
                mapBounds.extend(center);
                count++;
            }
        }
        if (polyPts.length >= 3) {
            this._addPolygonShape(polyPts, this.shapeColor, this.lineWidth);
        }

        /* center on geozone */
        var centerPt = DEFAULT_CENTER; // JSMapPoint
        var zoom     = DEFAULT_ZOOM;
        if (count > 0) {
            centerPt = mapBounds.getCenter(); // JSMapPoint
            zoom     = this._calcBestZoom(mapBounds);
        }
        this._setCenter(new VELatLong(centerPt.lat, centerPt.lon), zoom);

        /* current MPP */
        var mpp;
        if ((MAP_WIDTH > 0) && (MAP_HEIGHT > 0)) {
            mpp  = mapBounds.calculateMetersPerPixel(MAP_WIDTH, MAP_HEIGHT);
        } else {
            mpp  = mapBounds.calculateMetersPerPixel(680, 470); // TODO: read these values from the map
        }
        radiusM = 20.0 * mpp;
        jsvZoneRadiusMeters = radiusM;

        /* draw drag circles at vertices */
        for (var i = 0; i < polyPts.length; i++) {
            var c  = polyPts[i]; // OpenLayers.LonLat
            var vc = new VELatLong(c.lat,c.lon);
            var isPrimary = (i == polyPtsPrim);
            if (jsvZoneEditable || jsvShowVertices) {
                this._addCircleShape(vc, radiusM, this.shapeColor, this.lineWidth, isPrimary);
            }
            if (isPrimary) {
                this.geozoneCenter = vc;
            }
        }

    } else
    if (type == ZONE_SWEPT_POINT_RADIUS) {

        /* adjust radius */
        if (isNaN(radiusM))              { radiusM = 1000; }
        if (radiusM > MAX_ZONE_RADIUS_M) { radiusM = MAX_ZONE_RADIUS_M; }
        if (radiusM < MIN_ZONE_RADIUS_M) { radiusM = MIN_ZONE_RADIUS_M; }

        /* draw points */
        var count = 0;
        var mapBounds = new JSBounds();
        var polyPts = []; // JSMapPoint
        for (var i = 0; i < points.length; i++) {
            var c = points[i]; // JSMapPoint
            if ((c.lat != 0.0) || (c.lon != 0.0)) {
                var isPrimary = (i == primNdx);
                if (jsvZoneEditable || jsvShowVertices) {
                    this._addCircleShape(new VELatLong(c.lat,c.lon), radiusM, this.shapeColor, this.lineWidth, isPrimary);
                }
                polyPts.push(c); // JSMapPoint
                mapBounds.extend(c);
                mapBounds.extend(this._calcRadiusPoint(c, radiusM,   0.0));
                mapBounds.extend(this._calcRadiusPoint(c, radiusM,  90.0));
                mapBounds.extend(this._calcRadiusPoint(c, radiusM, 180.0));
                mapBounds.extend(this._calcRadiusPoint(c, radiusM, 270.0));
                count++;
            }
        }

        /* draw corridors */
        if (polyPts.length >= 2) {
            // routeline "_createRouteFeature"
            for (var i = 0; i < (polyPts.length - 1); i++) {
                var ptA = polyPts[i  ]; // JSMapPoint
                var ptB = polyPts[i+1]; // JSMapPoint
                var hAB = geoHeading(ptA.lat, ptA.lon, ptB.lat, ptB.lon) - 90.0; // perpendicular
                var rp1 = this._calcRadiusPoint(ptA, radiusM, hAB        ); // JSMapPoint
                var rp2 = this._calcRadiusPoint(ptB, radiusM, hAB        ); // JSMapPoint
                var rp3 = this._calcRadiusPoint(ptB, radiusM, hAB + 180.0); // JSMapPoint
                var rp4 = this._calcRadiusPoint(ptA, radiusM, hAB + 180.0); // JSMapPoint
                var rectPts = [ rp1, rp2, rp3, rp4 ];
                this._addPolygonShape(rectPts, this.shapeColor, this.lineWidth);
            }
        }

        /* center on geozone */
        var centerPt = DEFAULT_CENTER; // JSMapPoint
        var zoom     = DEFAULT_ZOOM;
        if (count > 0) {
            centerPt = mapBounds.getCenter(); // JSMapPoint
            zoom     = this._calcBestZoom(mapBounds);
        }
        this._setCenter(new VELatLong(centerPt.lat, centerPt.lon), zoom);

    } else {
        
        alert("Geozone type not supported: " + type);

    }
    
    return null;
};

/**
*** Calculate the best zoom
*** @param bounds JSBounds
**/
JSMap.prototype._calcBestZoom = function(bounds)
{
    // Derived from the zoom values specified at the following link:
    //  - http://blogs.msdn.com/virtualearth/archive/2006/02/25/map-control-zoom-levels-gt-resolution.aspx
    var mpp;
    if ((MAP_WIDTH > 0) && (MAP_HEIGHT > 0)) {
        mpp  = bounds.calculateMetersPerPixel(MAP_WIDTH, MAP_HEIGHT);
    } else {
        mpp  = bounds.calculateMetersPerPixel(680, 470); // TODO: read these values from the map
    }
    // Based on the ideal meters-per-pixel calculated by the JSBounds instance, the following converts
    // this value to the VirtualEarth Zoom#.
    var C = 0.2985821533203125000; // derived from MSVE zoom meters-per-pixel values
    // MPP  = C * 2^(19-ZOOM);   [where ZOOM is between 1 and 19, inclusive]
    // ZOOM = 19 - LOG2(MPP/C);  [where LOG2(X) == (LOGe(X)/LOGe(2))]
    var zoom = (19 - Math.round(Math.log(mpp / C) / Math.log(2.0))) - 1; // '-1' just to make sure everything fits
    if (zoom < 1) {
        return 1;
    } else
    if (zoom > 19) {
        return 19;
    } else {
        return zoom;
    }
};

/**
*** Returns an array of points (VELatLong) representing a circle polygon
*** @param center   The center point (VELatLong) of the circle
*** @param radiusM  The radius of the circle in meters
*** @return An array of points (VELatLong) representing a circle polygon
**/
JSMap.prototype._getCirclePoints = function(center, radiusM)
{
    var rLat = geoRadians(center.Latitude);   // radians
    var rLon = geoRadians(center.Longitude);  // radians
    var d    = radiusM / EARTH_RADIUS_METERS;
    var circlePoints = new Array();
    for (x = 0; x <= 360; x += 6) {       // 6 degrees (saves memory, and it still looks like a circle)
        var xrad = geoRadians(x);         // radians
        var tLat = Math.asin(Math.sin(rLat) * Math.cos(d) + Math.cos(rLat) * Math.sin(d) * Math.cos(xrad));
        var tLon = rLon + Math.atan2(Math.sin(xrad) * Math.sin(d) * Math.cos(rLat), Math.cos(d) - Math.sin(rLat) * Math.sin(tLat));
        circlePoints.push(new VELatLong(geoDegrees(tLat), geoDegrees(tLon)));
    }
    return circlePoints;
};

/**
*** Returns a circle shape (VEShape)
*** @param center   The center point (VELatLong) of the circle
*** @param radiusM  The radius of the circle in meters
*** @return The circle VEShape object
**/
JSMap.prototype._addCircleShape = function(center, radiusM, color, width, isPrimary)
{

    /* Circle points */
    var circlePoints = this._getCirclePoints(center, radiusM);

    /* Circle shape */
    var shape = new VEShape(VEShapeType.Polygon, circlePoints);
    shape.SetTitle('<small><b>'+center.Latitude+' / '+center.Longitude+' '+radiusM+'</b></small>');
    shape.SetLineWidth(width);
    shape.SetDescription("Click and drag");
    shape.HideIcon();  // remove the pushpin at the center of the circle

    /* save primary shape */
    var rgb = (color && (color != ""))? rgbVal(color) : null;
    if (isPrimary) {
        if (rgb) {
            shape.SetLineColor(new VEColor(  220,    0,    0, 1.00));
            shape.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B, 0.20));
        } else {
            shape.SetLineColor(new VEColor(200,  0,  0, 1.0));
          //shape.SetFillColor(new VEColor(  0,100,150, 1.0));
        }
        //shape.SetCustomIcon("<img src='...'>");
        //shape.ShowIcon();
        this.virtEarthGeozoneLayer.AddShape(shape);
        this.geozoneShape  = shape;
        this.geozoneCenter = center; // VELatLong
    } else {
        if (rgb) {
            //shape.SetLineColor(new VEColor( 0,  90,  0, 1.0)); // #005A00
            shape.SetLineColor(new VEColor(rgb.R,rgb.G,rgb.B, 0.35));
            shape.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B, 0.20));
        } else {
            shape.SetLineColor(new VEColor( 0,  90,  0, 0.35)); // #005A00
            shape.SetFillColor(new VEColor( 0, 180,  0, 0.20)); // #00B400
        }
        this.virtEarthGeozoneLayer.AddShape(shape);
        //this.virtEarthGeozoneTempLayer.AddShape(shape);
    }

    /* Icon */
    /*
    if (jsvZoneEditable) {
        var iconURL = "http://labs.google.com/ridefinder/images/mm_20_blue.png";
        //var iconHotspot = [6, 12];
        //var xOfs = VEPushpin_XOfs - iconHotspot[0];
        //var yOfs = VEPushpin_YOfs - iconHotspot[1];
        //var imgHTML = "<div style='position: relative; top: "+yOfs+"px; left: "+xOfs+"px'><img src='"+iconURL+"'></div>";
        //shape.SetCustomIcon(imgHTML);
        //shape.SetCustomIcon(iconURL);

        var centerMarker = new VEShape(VEShapeType.Pushpin, new VELatLong(center.Latitude, center.Longitude));
        centerMarker.SetTitle(" ");
        centerMarker.SetDescription("GeoZone");
        centerMarker.SetCustomIcon(iconURL);
        this.virtEarthGeozoneLayer.AddShape(centerMarker);
        //this.virtEarthGeozoneTempLayer.AddShape(centerMarker);

    }
    */
    
    return shape;

};

JSMap.prototype._calcRadiusPoint = function(center/*JSMapPoint*/, radiusM, heading)
{
    var pt = geoRadiusPoint(center.lat, center.lon, radiusM, heading); // { lat: <>, lon: <> }
    return new JSMapPoint(pt.lat, pt.lon);
};

/**
*** Returns a circle shape (VEShape)
*** @param center   The center point (VELatLong) of the circle
*** @param radiusM  The radius of the circle in meters
*** @return The circle VEShape object
**/
JSMap.prototype._addPolygonShape = function(points, color, width)
{

    /* polygon points */
    var polyPoints = [];
    for (var i = 0; i < points.length; i++) {
        polyPoints.push(new VELatLong(points[i].lat, points[i].lon));
    }

    /* shape */
    var shape = new VEShape(VEShapeType.Polygon, polyPoints);
    shape.SetTitle('<small><b>Polygon Geozone</b></small>');
    shape.SetLineWidth(width);
    shape.SetDescription("Click and drag");
    shape.HideIcon();  // remove the pushpin at the center

    /* save primary shape */
    var rgb = (color && (color != ""))? rgbVal(color) : null;
    if (rgb) {
        //shape.SetLineColor(new VEColor( 0,  90,  0, 1.0)); // #005A00
        shape.SetLineColor(new VEColor(rgb.R,rgb.G,rgb.B, 0.75)); // 0.35
        shape.SetFillColor(new VEColor(rgb.R,rgb.G,rgb.B, 0.20));
    } else {
        shape.SetLineColor(new VEColor( 0,  90,  0, 0.75)); // 0.35, #005A00
        shape.SetFillColor(new VEColor( 0, 180,  0, 0.20)); // #00B400
    }
    this.virtEarthGeozoneLayer.AddShape(shape);
    this.geozoneShape = shape;

};

// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------

/**
*** Mouse event handler to draw circles/lines on the map 
*** @param e  The mouse event
**/
JSMap.prototype._getGeozoneAtPoint = function(lat,lon)
{
    if ((this.geozoneCenter == null) || (this.geozoneShape == null)) {
        return null
    } else {
        var CC = this.geozoneCenter; // VELatLong
        var radiusM = zoneMapGetRadius(false);
        if (geoDistanceMeters(CC.Latitude,CC.Longitude,lat,lon) <= radiusM) {
            return this.geozoneShape;
        } else {
            return null;
        }
    }
};

/**
*** Mouse event handler to draw circles/lines on the map 
*** @param e  The mouse event
**/
JSMap.prototype._event_OnMouseDown = function(e)
{
    
    /* last mousedown X/Y */
    this.lastX = e.mapX;
    this.lastY = e.mapY;
    
    /* quick exits */
    if (!e.leftMouseButton || e.altKey || (e.ctrlKey && e.shiftKey)) {
        this.dragType = DRAG_NONE;
        return false;
    }

    /* mouse down point */
    var x = e.mapX, y = e.mapY;
    var LL = this.virtEarthMap.PixelToLatLong(new VEPixel(x,y));
    jsmapElem.style.cursor = 'crosshair';

    /* distance ruler */
    if (e.ctrlKey) {
        this.dragType = DRAG_RULER;
        this.dragRulerLatLon = LL;
        this.virtEarthRulerLayer.DeleteAllShapes();
        jsmSetDistanceDisplay(0);
        return true; // e.preventDefault()
    }

    /* geozone mode */
    if (jsvGeozoneMode && jsvZoneEditable) {
        // Note: We cannot believe the value of 'e.elementID on Safari, so we do not use it here!
        var geozone = this._getGeozoneAtPoint(LL.Latitude,LL.Longitude);
        if (geozone != null) {
            this.virtEarthMap.HideInfoBox();
            this.dragMarker = geozone;
            if (e.shiftKey) {
                // resize
                this.dragType = DRAG_GEOZONE_RADIUS;
                this.virtEarthRulerLayer.DeleteAllShapes();
            } else {
                // move 
                this.dragType = DRAG_GEOZONE_CENTER;
                var CC = this.geozoneCenter;
                this.dragZoneOffsetLat = LL.Latitude  - CC.Latitude;
                this.dragZoneOffsetLon = LL.Longitude - CC.Longitude;
                this.virtEarthMap.vemapcontrol.EnableGeoCommunity(true);
            }
            return true;
        }
    }
    
    this.dragType = DRAG_NONE;
    return false;
};

/**
*** Mouse event handler to draw circles on the map 
*** @param e  The mouse event
**/
JSMap.prototype._event_OnMouseUp = function(e)
{

    /* geozone mode */
    if (jsvGeozoneMode && (this.dragMarker != null)) {
        var center = this.geozoneCenter;
        this.dragMarker = null;
        jsmSetPointZoneValue(center.Latitude, center.Longitude, jsvZoneRadiusMeters);
        this.virtEarthMap.vemapcontrol.EnableGeoCommunity(false);
        this.dragType = DRAG_NONE;
        mapProviderParseZones(jsvZoneList);
        return true;
    }
        
    /* normal mode */
    this.dragType = DRAG_NONE;
    this.dragRulerLatLon = null;
    return false;

};

/**
*** Mouse event handler to detect lat/lon changes and draw circles/lines on the map 
*** @param e  The mouse event
**/
JSMap.prototype._event_OnMouseMove = function(e)
{
    var X = e.mapX;
    var Y = e.mapY;
    var LL = this.virtEarthMap.PixelToLatLong(new VEPixel(X, Y));

    /* update Latitude/Longitude display */
    if (this.latLonDisplay != null) {
        jsmSetLatLonDisplay(LL.Latitude,LL.Longitude);
        jsmapElem.style.cursor = 'crosshair';
    }

    /* disance ruler */
    if (this.dragType == DRAG_RULER) {
        this.virtEarthRulerLayer.DeleteAllShapes();
        var CC    = this.dragRulerLatLon;
        var distM = geoDistanceMeters(CC.Latitude, CC.Longitude, LL.Latitude, LL.Longitude);
        var line  = new VEShape(VEShapeType.Polyline, [CC,LL]);
        line.SetTitle('<small><b>'+distM+' meters</b></small>');
        //line.SetDescription('');
        line.HideIcon();
        line.SetLineWidth(2);
        line.SetLineColor(new VEColor(255,100,34,1)); // #FF6422
        line.SetFillColor(new VEColor(255, 34,34,1)); // #FF2222
        this.virtEarthRulerLayer.AddShape(line);
        jsmSetDistanceDisplay(distM);
        return true;
    }

    /* dragging the zone radius? */
    if (this.dragType == DRAG_GEOZONE_RADIUS) {
        this.virtEarthMap.HideInfoBox(this.dragMarker);
        this.virtEarthGeozoneLayer.DeleteAllShapes();
        //this.virtEarthGeozoneTempLayer.DeleteAllShapes();
        var CC = this.geozoneCenter; // VELatLong
        jsvZoneRadiusMeters = Math.round(geoDistanceMeters(CC.Latitude, CC.Longitude, LL.Latitude, LL.Longitude));
        if (jsvZoneRadiusMeters > MAX_ZONE_RADIUS_M) { jsvZoneRadiusMeters = MAX_ZONE_RADIUS_M; }
        if (jsvZoneRadiusMeters < MIN_ZONE_RADIUS_M) { jsvZoneRadiusMeters = MIN_ZONE_RADIUS_M; }
        this._addCircleShape(CC, jsvZoneRadiusMeters, jsvZoneColor, 2, true);
        jsmSetDistanceDisplay(jsvZoneRadiusMeters);
        this.virtEarthMap.HideInfoBox(this.dragMarker);
        return true;
    }
    
    /* dragging the zone center? */
    if (this.dragType == DRAG_GEOZONE_CENTER) {
        this.virtEarthMap.HideInfoBox(this.dragMarker);
        var circlePoints = this.dragMarker.GetPoints();
        this.geozoneCenter = new VELatLong(LL.Latitude - this.dragZoneOffsetLat, LL.Longitude - this.dragZoneOffsetLon);
        this.dragMarker.SetPoints(this._getCirclePoints(this.geozoneCenter, jsvZoneRadiusMeters));
        this.virtEarthMap.HideInfoBox(this.dragMarker);
        return true;
    }
    
    /* no-op */
    return false;

};

/**
*** Mouse event handler to recenter geozone
*** @param e  The mouse event
**/
JSMap.prototype._event_OnClick = function(e)
{

    /* quick exits */
    if (!e.leftMouseButton || e.altKey || e.ctrlKey || e.shiftKey) {
        // ignore 'shifts'
        return false;
    } else
    if ((e.mapX != this.lastX) || (e.mapY != this.lastY)) {
        // some 'dragging' has occurred
        return false;
    }

    /* geozone only */
    if (jsvGeozoneMode && jsvZoneEditable) {
        // recenter
        var x = e.mapX, y = e.mapY;
        var LL = this.virtEarthMap.PixelToLatLong(new VEPixel(x,y)); // VELatLong
        var CC = this.geozoneCenter? this.geozoneCenter : new VELatLong(0,0); // VELatLong
        var CCIsValid = ((CC.Latitude != 0.0) || (CC.Longitude != 0.0));
        var CCLLDistKM = geoDistanceMeters(CC.Latitude, CC.Longitude, LL.Latitude, LL.Longitude);
        if (jsvZoneType == ZONE_POINT_RADIUS) {
            var radiusM = zoneMapGetRadius(false);
            // inside primary zone?
            if (CCLLDistKM <= radiusM) {
                return false;
            }
            // inside any zone?
            if (this.geozonePoints && (this.geozonePoints.length > 0)) {
                for (var i = 0; i < this.geozonePoints.length; i++) {
                    //if (i == this.primaryIndex) { continue; }
                    var cc = this.geozonePoints[i]; // JSMapPoint
                    if (geoDistanceMeters(cc.lat, cc.lon, LL.Latitude, LL.Longitude) <= radiusM) {
                        return false;
                    }
                }
            }
            // outside geozone, recenter
            jsmSetPointZoneValue(LL.Latitude, LL.Longitude, jsvZoneRadiusMeters);
            mapProviderParseZones(jsvZoneList);
            return true;
        } else
        if (jsvZoneType == ZONE_POLYGON) {
            var radiusM = jsvZoneRadiusMeters;
            // inside primary zone?
            if (CCLLDistKM <= radiusM) {
                return false;
            }
            // inside any zone?
            if (this.geozonePoints && (this.geozonePoints.length > 0)) {
                for (var i = 0; i < this.geozonePoints.length; i++) {
                    //if (i == this.primaryIndex) { continue; }
                    var cc = this.geozonePoints[i];
                    if (geoDistanceMeters(cc.lat, cc.lon, LL.Latitude, LL.Longitude) <= radiusM) {
                        return false;
                    }
                }
            }
            // count number of valid points
            var count = 0;
            for (var z = 0; z < jsvZoneList.length; z++) {
                if ((jsvZoneList[z].lat != 0.0) || (jsvZoneList[z].lon != 0.0)) {
                    count++;
                }
            }
            if (count == 0) {
                // no valid points - create default polygon
                var radMeter = 450;
                var crLat    = geoRadians(LL.Latitude);   // radians
                var crLon    = geoRadians(LL.Longitude);  // radians
                for (x = 0; x < jsvZoneList.length; x++) {
                    var deg   = x * (360.0 / jsvZoneList.length);
                    var radM  = radMeter / EARTH_RADIUS_METERS;
                    if ((deg == 0.0) || ((deg > 170.0) && (deg<  190.0))) { radM *= 0.8; }
                    var xrad  = geoRadians(deg); // radians
                    var rrLat = Math.asin(Math.sin(crLat) * Math.cos(radM) + Math.cos(crLat) * Math.sin(radM) * Math.cos(xrad));
                    var rrLon = crLon + Math.atan2(Math.sin(xrad) * Math.sin(radM) * Math.cos(crLat), Math.cos(radM)-Math.sin(crLat) * Math.sin(rrLat));
                    _jsmSetPointZoneValue(x, geoDegrees(rrLat), geoDegrees(rrLon), 0);
                }
            } else {
                // move valid points to new location
                var deltaLat = LL.Latitude  - CC.Latitude;
                var deltaLon = LL.Longitude - CC.Longitude;
                for (var x = 0; x < jsvZoneList.length; x++) {
                    var pt = jsvZoneList[x];
                    if ((pt.lat != 0.0) || (pt.lon != 0.0)) {
                        _jsmSetPointZoneValue(x, (pt.lat + deltaLat), (pt.lon + deltaLon), 0);
                    }
                }
            }
            // reparse zone
            mapProviderParseZones(jsvZoneList);
            return true;
        } else
        if (jsvZoneType == ZONE_SWEPT_POINT_RADIUS) {
            var radiusM = jsvZoneRadiusMeters;
            // inside primary zone?
            if (CCLLDistKM <= radiusM) {
                return false;
            }
            // inside any zone?
            if (this.geozonePoints && (this.geozonePoints.length > 0)) {
                for (var i = 0; i < this.geozonePoints.length; i++) {
                    //if (i == this.primaryIndex) { continue; }
                    var cc = this.geozonePoints[i];
                    if (geoDistanceMeters(cc.lat, cc.lon, LL.Latitude, LL.Longitude) <= radiusM) {
                        return false;
                    }
                }
            }
            // count number of valid points
            var count = 0;
            var maxDistKM = 0.0;
            var lastPT = null;
            for (var z = 0; z < jsvZoneList.length; z++) {
                if ((jsvZoneList[z].lat != 0.0) || (jsvZoneList[z].lon != 0.0)) {
                    count++;
                    if (lastPT != null) {
                        var dkm = geoDistanceMeters(lastPT.lat, lastPT.lon, jsvZoneList[z].lat, jsvZoneList[z].lon);
                        if (dkm > maxDistKM) {
                            maxDistKM = dkm;
                        }
                    } else {
                        lastPT = jsvZoneList[z]; // first valid point
                    }
                }
            }
            var maxDeltaKM = ((maxDistKM > 5000)? maxDistKM : 5000) * 1.5;
            if (!CCIsValid || (count <= 0) || (CCLLDistKM <= maxDeltaKM)) {
                jsmSetPointZoneValue(LL.Latitude, LL.Longitude, radiusM);
            }
            // reparse zone
            mapProviderParseZones(jsvZoneList);
            return true;
        }
    }

    /* no-op */
    return false;

}; // _event_OnClick

// ----------------------------------------------------------------------------

/**
*** MouseOver event handler
*** @param e  The mouse event
**/
JSMap.prototype._event_OnMouseOver = function(e)
{

    /* no-op */
    return false;

};

/**
*** MouseOut event handler
*** @param e  The mouse event
**/
JSMap.prototype._event_OnMouseOut = function(e)
{

    /* no-op */
    return false;

};

// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
