﻿/** @class TNRIS.MapPanel
@param {object} config name/value properties to initialize with
@config {VELatLong} mapCenterStart centerpoint when map opens - defaults to 32,-100
@config {int} mapZoomStart default zoom level (between 1 and 19) - defaults to 6
@config {string} mapEl element ID passed to VEMap constructor
*/
TNRIS.MapPanel = function(config) {
    this.mapCenterStart = new VELatLong(32, -100);
    this.mapZoomStart = 6;
    /** distance in pixels between a mousedown & mouseup to consider a click */
    this.mapClickBuffer = 2;
    
    //HACK: double click delay should trigger a clickCancelled event - not delay issuance of a click event - slows down the interface
    /** time in miliseconds between clicks to consider a double click */
    this.doubleClickDelay = 500;
    
    this.layers = new Ext.util.MixedCollection();
    
    TNRIS.SelectionManager.on({
        'add': {scope: this, fn: this.onSelectionAddition},
        'remove': {scope: this, fn: this.onSelectionRemoval},
        'beforeunselectlayer': {scope: this, fn: this.onUnselectLayer},
        'selection' : { scope: this, fn: this.onSelection}
    });
    
    Ext.applyIf(this, {
        selectedShapeLineColor: new VEColor(0,250,250,0.6),
        selectedShapeFillColor: new VEColor(0,250,250,0.6),
        selectedShapeIcon: '<div class="globePushpin"><img src="images/pushPins/ochreGlobePin.png" /></div>'
    });
    
    /** collection of locations drawn on the map */
    this.locationRecord = Ext.data.Record.create([
        {name: 'LatLong', mapping: 'LatLong'},
        {name: 'Name', mapping: 'Name'},
        {name: 'shape', mapping: 'shape'}]);
    this.locationStore = new Ext.data.SimpleStore({
        fields: [{name: 'LatLong', mapping: 'LatLong'},
        {name: 'Name', mapping: 'Name'},
        {name: 'shape', mapping: 'shape'}]
    });
    this.locationStore.on('load', function() { this.saveState(); }, this);
    this.locationStore.on('remove', function() { this.saveState(); }, this);
        
    this.locationZIndex = 500;
    this.locationIcon = '<div class="locationPushpin globePushpin"><img src="images/pushPins/aquaGlobePin.png" /></div>';
    this.selectedLocationIcon = '<div class="locationPushpin selected globePushpin"><img src="images/pushPins/ochreGlobePin.png" /></div>';
    
    /** collection of directions drawn on the map*/
    this.directionRecord = Ext.data.Record.create([
        {name: 'From', mapping: 'From'},
        {name: 'To', mapping: 'To'},
        {name: 'Distance', mapping:'Distance'},
        {name: 'Time', mapping: 'Time'},
        {name: 'Itinerary', mapping: 'Itinerary'} ]);
    
    this.directionStore = new Ext.data.SimpleStore({
        fields: [
            {name: 'From', mapping: 'From'},
            {name: 'To', mapping: 'To'},
            {name: 'Distance', mapping:'Distance'},
            {name: 'Time', mapping: 'Time'},
            {name: 'Itinerary', mapping: 'Itinerary'}
        ]
    });
    
    this.directionStore.on('load', function() { this.saveState(); }, this);
    this.directionStore.on('remove', function() { this.saveState(); }, this);

    Ext.apply(this, config, {
        stateful: true
    });
    
    this.addEvents({
        'layer-change' : true,
        'map-change' : true, // fired after layers added or removed
        'view-change' : true, // fired after center or zoom level changes
        'state-change': true, // fired after misc changes that should be stored in state
        'shape-change':true, //status bar listen's to this event
        'beforemapclick': true,
        'mapclick': true,
        'beforemapdrag': true,
        'mapdragging': true,
        'mapdragdone': true,
        'mapmouseover': true,
        'mapmouseout': true,
        'mapmousemove': true,
        'mapdoubleclick': true,
        'mapcontextclick': true // fired when a right click occurs on the map
    });
    
    this.legend = new TNRIS.LayerLegend({
        id: "layerLegend",
        floating: true,
        title: 'Active Layers',
        autoScroll: true,
        width: 250,
        height: 290, 
        collapsedWidth: 250,
        mapPanel: this
    });
    
    TNRIS.MapPanel.superclass.constructor.call(this, {
        items: [
            {
                contentEl: this.mapEl,
                bodyStyle: {
                    overflow: 'hidden',
                    position: 'relative'
                },
                anchor: '100% 100%'
            },
            this.legend,
            new TNRIS.MapToolbar({
                id: 'mapToolbarPanel',
                mapPanel: this
            })
        ],
        bbar: new TNRIS.MapStatusbar({
            id: 'mapStatusbar',
			cls: 'mapStatus',
            mapPanel: this
        }),
        
        listeners: {
            'resize': {scope: this, fn: this.onResize },
            'render': {scope: this, fn: this.postRenderListener }
        },
        stateEvents: ['map-change', 'view-change', 'state-change']
    });
    
    /** called this.doubleClickDelay miliseconds after a click if no other click is received */
    this.delayedClickHandler = new Ext.util.DelayedTask(this.onMapClick, this);
    
    /** coordinates of mousedown */
    this.mouseDownAt = null;
    
    var map = this.getMap();
    map.AttachEvent("onchangeview", function(mapEvent) {
        this.fireEvent('view-change', this.getMap());
    }.bind(this));
    
    map.AttachEvent("onmousedown", function(mapEvent, e) {
        //console.log('onmousedown: '+ new Date().getMilliseconds());
        this.mouseDownAt = [mapEvent.mapX, mapEvent.mapY];
        this.lastMouseLocation = this.getMap().PixelToLatLong(new VEPixel(mapEvent.mapX, mapEvent.mapY));
        this.mouseIn = mapEvent.elementID;
        this.leftMouseDown = mapEvent.leftMouseButton;
        return false;
    }.bind(this));
    
    map.AttachEvent('onmouseup', function(mapEvent, e) {
        //console.log('onmouseup: '+ new Date().getMilliseconds());
        if (this.mouseDownAt == null) {
            // IE bug - double clicks get no second mouseup
            return false;
        }
        
        if (this.dragStarted) {
            var currentLoc = this.getMap().PixelToLatLong(new VEPixel(mapEvent.mapX, mapEvent.mapY));
            var delta = {latitude: currentLoc.Latitude - this.lastMouseLocation.Latitude, longitude: currentLoc.Longitude - this.lastMouseLocation.Longitude};
            this.lastMouseLocation = currentLoc;
            this.fireEvent('mapdragdone', mapEvent, delta);
            this.dragStarted = false;
        } else {
            if (mapEvent.rightMouseButton) {
                return false; // eas - Right mouse button functionality handled by onClick event (no sloppy right mouse clicks allowed)
            }
            if (this.fireEvent('beforemapclick', mapEvent, this.mouseIn)) { // beforemapclick is non-delayed
                this.delayedClickHandler.delay(this.doubleClickDelay, null, null, [mapEvent, this.mouseIn]); // mapclick is delayed to avoid firing event on doubleclick
            }
        }
            
        this.leftMouseDown = false;
        return false;
    }.bind(this));
    
    map.AttachEvent('onclick', function(mapEvent) {
        // console.log('onclick: '+ new Date().getMilliseconds());
        if (null != this.customCursor) {
            Ext.fly(this.mapEl).setStyle({cursor: this.customCursor});
        }
        if (mapEvent.rightMouseButton) {
            return !this.fireEvent('mapcontextclick', mapEvent);
        } else {
            return false;
        }
    }.bind(this));
    
    map.AttachEvent('ondoubleclick', function(mapEvent) {
        // console.log('ondoubleclick: '+ new Date().getMilliseconds());
        this.delayedClickHandler.cancel();
        return !this.fireEvent('mapdoubleclick', mapEvent, this.mapClicks);
    }.bind(this));

    map.AttachEvent('onmousemove', function(mapEvent) {
        //HACK: mapEvent.leftMouseButton seems to always be true.  this.mouseDownAt will be set on mouseDown and cleared on mouseUp

        var mapPanning = false;
        if (this.shiftKeyDragging) {
            if (this.leftMouseDown && mapEvent.shiftKey ) { mapPanning = true;}
        } else {
            if (this.leftMouseDown) { mapPanning = true;}
        }
        
        if (null != this.customCursor && !mapPanning) {
            Ext.fly(this.mapEl).setStyle({cursor: this.customCursor});
        }
        
        //until the operation (mapmove or mapdragging) is decided , do not pan the map
        if (this.leftMouseDown && !this.dragStarted &&
            ((Math.abs(mapEvent.mapX - this.mouseDownAt[0]) <= this.mapClickBuffer) ||
            (Math.abs(mapEvent.mapY - this.mouseDownAt[1]) <= this.mapClickBuffer)))
        {
            return true;
        }
        
        if (this.leftMouseDown && !this.dragStarted &&
            ((Math.abs(mapEvent.mapX - this.mouseDownAt[0]) > this.mapClickBuffer) ||
            (Math.abs(mapEvent.mapY - this.mouseDownAt[1]) > this.mapClickBuffer)))
        {
            this.dragStarted = true;
            mapEvent.mapX = this.mouseDownAt[0];
            mapEvent.mapY = this.mouseDownAt[1];
            return !this.fireEvent('beforemapdrag', mapEvent);
        }
        
        if (this.dragStarted)
        {
            var currentLoc = this.getMap().PixelToLatLong(new VEPixel(mapEvent.mapX, mapEvent.mapY));
            var delta = {latitude: currentLoc.Latitude - this.lastMouseLocation.Latitude, longitude: currentLoc.Longitude - this.lastMouseLocation.Longitude};
            this.lastMouseLocation = currentLoc;
            this.mouseDownAt = [mapEvent.mapX, mapEvent.mapY];
            return !this.fireEvent('mapdragging', mapEvent, delta);
        } else {
            return !this.fireEvent('mapmousemove', mapEvent);
        }
    }.bind(this));
    
    map.AttachEvent('onmouseover', function(mapEvent) {
        return !this.fireEvent('mapmouseover', mapEvent);
    }.bind(this));

    map.AttachEvent('onmouseout', function(mapEvent) {
        return !this.fireEvent('mapmouseout', mapEvent);
    }.bind(this));

    this.statusBar = Ext.getCmp('mapStatusbar');
    this.geoCalculator = new TNRIS.GeoCodeCalc();
};

Ext.extend(TNRIS.MapPanel, Ext.Panel, 
    /** @scope TNRIS.MapPanel */
    {
    
    getState: function() {
        var state = TNRIS.MapPanel.superclass.getState.call(this) || {};

        // save map view
        var center = this.getMap().GetCenter();
        state.view = {};
        state.view.c = [center.Latitude, center.Longitude];
        state.view.zm = this.getMap().GetZoomLevel();
        
        // save layers
        var layerState = [];
        Ext.each(this.getLayers(), function(layer) {
            if (layer.layerId() != null) {
                layerState.push({
                    id: layer.layerId(), 
                    vis: layer.visible(),
                    op: layer.opacity()
                });
            }
        }, this);
        
        if (layerState.length > 0) {
            state.layers = layerState;
        }
        
        // save locations
        var locationState = [];
        this.getLocationStore().each(function(record) {
            locationState.push({la: record.get('LatLong').Latitude, lo: record.get('LatLong').Longitude, na: record.get('Name')});
        }, this);
        
        if (locationState.length > 0) {
            state.loc = locationState;
        }
        
        //save directions
        var directionState = [];
        this.getDirectionStore().each(function(record){
            directionState.push({from: record.get('From'), to: record.get('To'),dist:record.get('Distance'),
                                time:record.get('Time'),itn: record.get('Itinerary')});
        },this);
        
        if (directionState.length >0) {
            state.dir = directionState;
        }
        return state;
    },

    applyState: function(state, config) {
        if (state) {
            this.state = state;
        }
    },
    
    restoreState: function(state) {
        // restore map view
        if (state.view != null) {
            this.getMap().SetCenterAndZoom(new VELatLong(state.view.c[0], state.view.c[1]), state.view.zm);
        }
            
        if (state.loc && state.loc.length > 0) {
            Ext.each(state.loc, function(loc) {
                this.addLocation({Name: loc.na, LatLong: new VELatLong(loc.la, loc.lo)}, true);
            }, this);
        }
        
        if (state.dir && state.dir.length >0) {
            Ext.each(state.dir, function(dir) {
                this.addDirection(dir.from, dir.to, dir.dist, dir.time, dir.itn);
            },this);
        }

        if (state.layers && state.layers.length > 0) {
            Ext.each(state.layers, function(_layer) {
                var id = _layer.id;
                var layer = TNRIS.LayerFactory.getLayerById(id);
                layer.opacity(_layer.op);
                layer.visible(_layer.vis);
                this.addLayer(layer);
            }, this);
        }
    },
    
    shapeStatusChange: function(shapeAttributes) {
        this.statusBar.setShapeStatus(shapeAttributes);
    },
    
    shapeStatusClear: function() {
        this.statusBar.clearShapeStatus();
    },
    
    showBusy: function(config) {
        this.statusBar.showBusy(config);
    },
    
    clearStatus: function() {
        this.statusBar.clearStatus();
    },
    
    onResize: function(adjWidth, adjHeight) {
        if (typeof(adjWidth) != 'number' || typeof(adjHeight) != 'number') {
            return;
        }
        var legend = this.legend;
        var map = this.getMap();
        var toolPanel = this.findById('mapToolbarPanel');
        var statusBar = this.getBottomToolbar();
        var infoPanel = Ext.getCmp('infoPanel');
        
        if(legend.rendered && toolPanel.rendered) {
            if (typeof(this.subpanelsRendered) != 'undefined') {
                this.subpanelsRendered = true;
                this.getMap().AddControl(legend.el.dom);
                this.getMap().AddControl(toolPanel.el.dom);
            }
            
            // Map only occupies the body.  The adjHeight is for the MapPanel (status bar, etc)
            var bodyHeight = adjHeight - statusBar.getSize().height;
            var bodyWidth = this.getSize().width;
            // Virtual Earth 6.0 puts 5px padding on all four sides of mapTop div
            map.Resize(bodyWidth - 10, bodyHeight - 10);
            
            var legendHeight = legend.getSize().height;
            
            // Shift the legend up high enough to expose the Microsoft copyright info
            var legendBottomOffset = 40;
            var vertPos = bodyHeight - legendHeight - legendBottomOffset;
            legend.setPosition(15, vertPos);
            toolPanel.setPosition(15, 10);
            
            // restore state - after the first time the map is resized
            if (this.state != null) {
                var state = this.state;
                // wait a second so the map has time to redraw;
                (new Ext.util.DelayedTask(this.restoreState, this, [state])).delay(500);
                this.state = null;
            }
        } 
        else 
        {
            (new Ext.util.DelayedTask(this.onResize, this, [adjWidth, adjHeight]).delay(100));
        }
    },
    
    /** Add a layer to the map.  Adding is delegated to LayerBase.addToMap.  If addToMap returns false, the addLayer call is cancelled
    @param {TNRIS.LayerBase} layer the layer to add.  
    */
    addLayer: function(_layer) {
        var id = Ext.id(null, 'mapItem');
        var highestIndex = 10;
        if (this.layers.getCount() > 0) {
            this.layers.each(function(layer) {
                highestIndex = Math.max(highestIndex, layer.index());
            });
        }
        var index = highestIndex + 1;
        _layer.on('loadmap', function () {
            this.layers.add(id, _layer);
            this.fireEvent('map-change', {'add': _layer });
        }, this, {single: true});
        
        // TODO: after saving shapes, return the oldId with the new ID if they changed to update selections
        // Any selected shapes from an unsaved layer will need to be removed once saved (there's no way to correlate clientIds with serverIds - YET)
        if (!_layer.isPersisted()) {
            _layer.on('metadatachange', TNRIS.SelectionManager.unselectByLayer, TNRIS.SelectionManager, {single: true, layer: _layer});
        }
        _layer.addToMap(id, this.getMap(), index, this);
    },
    
    removeLayer: function(id) {
        var layer = this.layers.get(id);
        
        // unselect any selected shapes from this layer
        TNRIS.SelectionManager.unselectByLayer(layer);
        
        //TODO: added isEditable condition to ignore smart layer being saved. remove this comment after testing
        if ((!layer.isPersisted() || layer.isModified())&& layer.isEditable()) { 
            Ext.Msg.confirm("Warning!", "This layer is not saved.  Do you wish to save this layer?", function(btnId) {
                if (btnId == 'yes') {
                    var win = new TNRIS.UserLayerWindow({
                        layer: layer, 
                        mapPanel: this,
                        listeners: {'close': {scope: this, fn: function() {this._removeLayer(id,layer);}}}});
                    win.show();
                } else {
                    this._removeLayer(id,layer);
                }
            }, this);
        } else {
            this._removeLayer(id,layer);
        }
    },
    
    _removeLayer: function(id, layer) {
        this.layers.removeKey(id);
        layer.removeFromMap();
        layer.dispose();
        gemss.app.msg('Layer Removed', "The '{0}' layer has been removed", layer.name());
        this.fireEvent('map-change', {'remove': id});
    },

    /* return array copy of the active layers in ascending order */
    getLayers: function() {
        return this.layers.getRange().sort(function(a,b) {
            return (a.index() - b.index());
        });
    },

    getLayer:function(_name) {
        return this.layers.get(_name);
    },
    
    getSearchableLayers: function() {
        var layerList = [];
        this.layers.each(function(layer) {
            if (layer.searchable()) { layerList.push(layer.layerId()); }
        });
        return layerList;
    },
    
    getLayerById: function(id) {
        return this.layers.find(function(layer) { return layer.layerId() == id; });
    },

  /********************
  * Layer Ordering 
  ********************/
  // layerIdArray is a list of layer Id's ordered from the bottom up
    setLayerOrder: function(mapIdArray) {
        mapIdArray.each(function(id, index) {
            var newMapIndex = index + 10;
            this.getLayer(id).index(newMapIndex);
        }, this);
        this.fireEvent('state-change');
    },

    getLayerOrder: function() {
        this.layers.sort('ASC', function(l1, l2) { 
            return l1.index() > l2.index() ? 1 : (l1.index() < l2.index() ? -1 : 0);
        });
        var sortedmapIds = [];
        this.layers.each(function(item) { sortedmapIds.push(item.mapId());});
        return sortedmapIds;
    },

    /********************************
    * Map Events
    ********************************/
    onMapClick: function(mapEvent) {
        this.fireEvent('mapclick', mapEvent, this.mouseIn);   
    },
    
    onLayerChange: function(layer) {
        this.fireEvent('layer-change', layer);
    },
    
    /********************************
    * Custom Cursors
    *********************************/
    
    /** a custom cursor for the mapPanel that will override the mouseup cursor style set by Virtual Earth */
    setCustomCursor: function(value,shiftKeyDragging) {
        this.customCursor = value;
        
        //used to set hand cursor when panning the map with shift key when normal dragging is overridden eg. rectangular tool
        this.shiftKeyDragging = shiftKeyDragging; 
        Ext.fly(this.mapEl).setStyle({cursor: this.customCursor});
    },
    
    /** returns the active cursor */
    getCustomCursor: function(){
        return this.customCursor;
    },
    
    /** revert to the default Virtual Earth Cursors */
    clearCustomCursor: function() {
        this.customCursor = null;
        this.shiftKeyDragging = null;
        Ext.fly(this.mapEl).setStyle({cursor: 'url(http://dev.virtualearth.net/mapcontrol/v6/cursors/grab.cur)'});
    },

    /********************************
    * Locations
    ********************************/
    getLocationLayer: function() {
        if (this.locationLayer == null) {
            this.locationLayer = new VEShapeLayer();
            this.getMap().AddShapeLayer(this.locationLayer);
        }
        return this.locationLayer;
    },
    
    /* 
    NOTE: simplified place objects are stored in the stateMgr.  
    If using more than the LatLong and Name properties on the place object, update onStateSave
    and onStateRestore to store those extra properties as well 
    */ 
    addLocation: function(place, skipShowBox) {
        var shape = new VEShape(VEShapeType.Pushpin, place.LatLong);
        this.getLocationLayer().AddShape(shape);
        shape.SetCustomIcon(this.locationIcon);
        shape.SetDescription(place.Name);
        shape.SetZIndex(this.locationZIndex);
        if (!skipShowBox) {
            this.getMap().ShowInfoBox(shape);
        }
        place.shape = shape;
        var record = new this.locationRecord({Name: place.Name, LatLong: place.LatLong, shape: shape});
        this.getLocationStore().add([record], true);  
    },
    
    removeLocation: function(placeRecord) {
        this.getLocationLayer().DeleteShape(placeRecord.get('shape'));
        this.locationStore.remove(placeRecord);
    },
    
    getLocationStore: function() {
        return this.locationStore;
    },
    
    /**************************************
    *Directions
    ***************************************/
    addDirection: function(from,to,dist,time,itinerary) {
        var recordProcessed = false;
        var store = this.getDirectionStore();
        
        store.each(
            function(record,id){
                if((record.get('From') == from) && (record.get('To') == to)) {
                    record.set('Distance',dist);
                    record.set('Time',time);
                    record.set('Itinerary',itinerary);
                    recordProcessed = true;
                    return false;
                }
            },this);
        
        if (! recordProcessed) {
            var record = new this.directionRecord(
                {From: from, To : to, Distance: dist, Time: time, Itinerary: itinerary });
            store.add([record],true);
        }
    },
    
    removeDirection: function(directionRecord) {
        this.getDirectionStore().remove(directionRecord);
    },
    
    getDirectionStore: function() {
        return this.directionStore;
    },
    
    /** unhighlights existing locations & highlights this location */
    selectLocation: function(location) {
        this.locationStore.each(function(record) {
            var s = record.get('shape');
            if (s != null) {
                s.SetCustomIcon(this.locationIcon);
            }
        }, this);
        this.applySelectedShapeStyling(location.get('shape'));
    },


    /********************************
    * Custom Shapes
    ********************************/
    
    getActiveLayer: function() {
        // determine active shape layer from layer legend selected
        // if no active layer, create a new shapelayer
        var activeShapeLayer = null;
        this.layers.each(function(layer) {
            if (!layer.isEditable() || !layer.visible()) {
                return;
            }
            if (activeShapeLayer == null || activeShapeLayer.index() < layer.index()) {
                activeShapeLayer = layer;
            }
            var t = activeShapeLayer;
        }, this);
        if (activeShapeLayer == null) {
            activeShapeLayer = TNRIS.LayerFactory.createLayer({source_type: 'xml', micronail_url: 'images/testServices/micronails/user_layer_master.png' });
            this.addLayer(activeShapeLayer);
        }
        return activeShapeLayer;
    },
    
    /** find the TNRIS.Layer a shape belongs to */
    getLayerByShape: function(shape) {
        var result = null;
        var shapeLayer = shape.GetShapeLayer();
        this.layers.each(function(layer) {
            if (shapeLayer == layer.getShapeLayer()) {
                result = layer;
                return false;
            } else {
                return true;
            }
        }, this);
        return result;
    },
    
    /* get the Virtual Earth Shape Object from a shape or shapeId
    @param {String || VEShape} shapeOrId
    @returns {VEShape}
    */
    getShape: function(shapeOrId) {
        var shape = shapeOrId;
        if (typeof(shapeOrId) == "string") {
            shape = this.getMap().GetShapeByID(shapeOrId);
        }
        return shape;
    },
    
    getSelectedShapes: function() {
        var shapes = [];
        TNRIS.SelectionManager.each(function(selectionEntry) {
            var shape = selectionEntry.layer.getShapeByRecordId(selectionEntry.recordId);
            shape && shapes.push(shape);
        }, this);
        return shapes;
    },
    
    isSelectedShape: function(_shape) {
        var shape = this.getShape(_shape);
        var layer = this.getLayerByShape(shape);
        var record = layer.getRecordByShape(shape);
        return TNRIS.SelectionManager.isSelectedRecord(layer, record);
    },
    
    selectShape: function(_shape) {
        var shape = this.getShape(_shape);
        if (!this.isSelectedShape(shape)) {
            var layer = this.getLayerByShape(shape);
            var record = layer.getRecordByShape(shape);
            TNRIS.SelectionManager.selectRecord(layer, record);
        }
    },
    
    unselectShape: function(_shape) {
        var shape = this.getShape(_shape);
        if (shape != null && this.isSelectedShape(shape)) {
            var layer = this.getLayerByShape(shape);
            var record = layer.getRecordByShape(shape);
            TNRIS.SelectionManager.unselectRecord(layer, record);
        }
    },
    
    clearSelections: function() {
        TNRIS.SelectionManager.unselectByFunction(function(layer, recordId) {
            var shape = layer.getShapeByRecordId(recordId);
            return (shape != null);
        }, this);
    },
    
    toggleSelectShape: function(_shape) {
        var shape = this.getShape(_shape);
        if (this.isSelectedShape(shape)) {
            this.unselectShape(shape);
        } else {
            this.selectShape(shape);
        }
    },
    
    onSelectionRemoval: function(o) {
        var shape = o.layer.getShapeByRecordId(o.recordId);
        shape && o.layer.applyShapeStyling(shape);
    },
    
    onSelectionAddition: function(index, o) {
        var shape = o.layer.getShapeByRecordId(o.recordId);
        shape && this.applySelectedShapeStyling(shape);
    },
    
    onUnselectLayer: function(layer) {
        return !this.layers.contains(layer);
    },
    
    onSelection: function() {
        if (null == this.shapeStatusReqTimer) {
            this.shapeStatusReqTimer = new Ext.util.DelayedTask();
        }
        
        //contextID notifies the change of selected shapes between the server call and the callback
        this.contextID = Ext.util.Format.date(new Date(), 'H:i:s:u');
        this.shapeStatusReqTimer.delay(500,
            function() {
                var selected = this.getSelectedShapes();
                if (selected.length >0 ) {
                    this.showBusy("Calculating shape attributes...");
                    this.geoCalculator.calcPersistedShapeAttributes(selected,this.contextID, function(result) {
                        if ( this.contextID == result.contextId) { 
                            this.shapeStatusChange(result); 
                        }
                        else { 
                            this.shapeStatusClear(); 
                        }
                        this.clearStatus();
                    }.bind(this));
                }
            else {
                this.shapeStatusClear();
            }
        },this);
    },

    deleteSelectedShapes: function() {
        TNRIS.SelectionManager.unselectByFunction(function(layer, recordId) {
            var shape = layer.getShapeByRecordId(recordId);
            if (shape != null) {
                layer.deleteShape(shape);
            }
        }, this);
    },
    
    modifySelectedShapes: function() {
        var selected = this.getSelectedShapes();
        Ext.each(this.getLayers(), function(layer) {
            layer.modifyShapes(selected);
        }, this);
    },
    
    applySelectedShapeStyling: function(shape) {
        shape.SetFillColor(this.selectedShapeFillColor);
        shape.SetLineColor(this.selectedShapeLineColor);
        if (shape.GetType() == "Point") {
            shape.SetCustomIcon(this.selectedShapeIcon);
        }
    },  
    
    /***********************************************
    * accessors & layout functions
    ***********************************************/

    postRenderListener: function() {
        // setup drop target
        this.dropZone = new Ext.dd.DropTarget(this.mapEl, {
            ddGroup: 'mapLayer',
            mapPanel: this
        });
    },
    
    getMap: function() {
        if (! this.map) {
            this.map = new VEMap(this.mapEl);
            var center = this.mapCenterStart;
            var zoom = this.mapZoomStart;
            if (this.state && this.state.view) {
                center = this.state.view.c;
                zoom = this.state.view.zm;
            }
            this.map.LoadMap(this.mapCenterStart, this.mapZoomStart);
            this.map.HideDashboard();
            this.map.SetMapStyle(VEMapStyle.Shaded);
            this.map.vemapcontrol.SetAnimationEnabled(false);
        }
        return this.map;
    },
    
    /*  Map Navigation */
    zoomToLayer: function(layer) {
        this.getMap().SetMapView(layer.footprintAsLongRectangle());
    }
    
});

TNRIS.SelectionManager = Ext.apply(new Ext.util.MixedCollection(), {
    init: function() {
        this.addEvents({'selection': true, 'beforeunselectlayer': true});
        this.on({
            'add': { scope: this, fn: function() { this.fireEvent('selection'); }},
            'remove': {scope: this, fn: function() { this.fireEvent('selection'); }},
            'clear': {scope: this, fn: function() { this.fireEvent('selection'); }}
        });
    },
    
    isSelectedRecord: function(layer, record) {
        return this.containsKey(layer.id + "." + record.id);
    },
    
    selectRecord: function(layer, _record) {
        var record = typeof(_record) == "object" ? _record.id : _record;
        if (!this.isSelectedRecord(layer,record)) {
            this.add(layer.id + "." + record, {layer: layer, recordId: record});
        }
    },
    
    unselectByFunction: function(callback, scope) {
        items = [];
        this.each(function(item) {
            if (callback.call(scope, item.layer, item.recordId)) {
                items.push([item.layer, item.recordId]);
            }
        }, this);
        this.unselectRecords(items);
        return items;
    },
    
    unselectRecord: function(layer, _record) {
        var record = typeof(_record) == "object" ? _record.id : _record;
        this.removeKey(layer.id + "." + record);
    },
    
    unselectRecords: function(items) {
        Ext.each(items, function(item) {
            this.unselectRecord(item[0], item[1]);
        }, this);
    },
    
    getSelectedRecords: function(layer) {
        var records = [];
        this.each(function(selection) {
            if (selection.layer == layer) {
                records.push(layer.getRecord(selection.recordId));
            }
        }, this);
        return records;
    },
     
    /** remove selections that are members of the passed in layer */    
    unselectByLayer: function(layer) {
        if (this.fireEvent('beforeunselectlayer', layer)) {
            var removals = this.filterBy(function(value, key) {
                return value.layer == layer;
            }, this);
            removals.each(function(o) {
                this.remove(o);
            }, this);
        }
    }

});

if (typeof(Sys) !== "undefined") { Sys.Application.notifyScriptLoaded(); }

