﻿/**
@class A Layer backed by a spatial database
@constructor
@augments TNRIS.LayerBase
*/

//global function to invoke a method on a component
function ShapeLayerFunctionProxy(methodName, cmpId, args) {
    Ext.getCmp(cmpId)[methodName](args);
}

TNRIS.ShapeLayer = function(config) {
    this.baseParams = {};

    Ext.applyIf(this, {
        shapeLineColor: new VEColor(0,150,150,0.6),
        shapeFillColor: new VEColor(0,150,150,0.6),
        shapeIcon: '<div class="hurrtraxPushpin"><img src="images/pushPins/redHurrTrax.png" /></div>'
    });

    TNRIS.ShapeLayer.superclass.constructor.call(this, config );

    // store VEShapes between shapeStore.load & refresh - saves deleting and recreating shapes
    this.cachedVEShapes = new Ext.util.MixedCollection();    
    
    this.stores = new Ext.util.MixedCollection();
    this.storesSyncing = [];
};

Ext.extend(TNRIS.ShapeLayer, TNRIS.LayerBase, 
    /** @scope TNRIS.ShapeLayer */
    {
    
    /***************************
    * Layer Options 
    ****************************/
    /** returns true if layer points should be refected with each change of view extent */
    isDisplayedDynamically: function() {
        // TODO - calculate this based on type?
        return true;
    },

    isEditable: function() {
        // TODO : calculate editability based on user role & other settings)
        return false;
    },

    /** shapelayers are queryable by default */
    queryable: function(value) {
        return 1;
    },
    
    /** shape layers can be made into smart layers */
    smartable: function(value) {
        return true;
    },
    
    /** shape layers can't alter their opacity */
    isOpacityVariable: function(value) {
        return false;
    },

    suggestSmartLayer: function() {
        return this.getShapeStore().getTotalCount() > 100;
    },
    
    /** True if data should be loaded from the server when the layer is first added */
    doLoadOnAdd: function() {
        return (null != this.layerId() && !this.deferLoadOnAdd);
    },
    
    columnConfigs: function() {
        return this.defaultMethod('columnConfigs');
    },
    
    /*******************************
    * Map Interactions
    *******************************/    
    /** add this layer to the map. */
    addToMap: function(id, map, index, mapPanel) {
        this.mapId(id);
        this.map(map);
        this.mapPanel(mapPanel);
        this.index(index);
        var store = this.getShapeStore();
        store.on('update', this.refreshShape, this);
        store.on('load', this.refresh, this);
        this.fireEvent('loadmap');
        if (this.isDisplayedDynamically()) {
            mapPanel.on('view-change', this.load, this);
        }
        if (this.doLoadOnAdd()) {
            // load the store, alert beforeloadmap listeners, then fire loadmap
            if (store.getCount() > 0) {
                this.refresh();
            } else {
                this.load(map);
            }
        }
    },
    
    /** 
    sets or gets the order this layer is drawn in relation to other tiled layers
    @param {integer} [value] the draw order this layer should use
    */
    index: function(value) {
        var current = this.defaultMethod('zindex');
        if (typeof(value) != 'undefined' && value != current) {
            if (this.map()) {
                this.getShapeStore().each(function(record) {
                    var shape = this.getShapeByRecord(record);
                    shape && shape.SetZIndex(value, value);
                }, this);
                this.redraw();
            }
        }
        return this.defaultMethod('zindex', value);
    },

    /** remove this layer from the map. */
    removeFromMap: function() {
        this.destroyShapeLayer();
        // TODO : remove & destroy the mapshape store
        this.mapPanel().removeListener('view-change', this.load, this);
        this.removeStore(this.getShapeStore());
        this.cachedVEShapes.clear();
        TNRIS.ShapeLayer.superclass.removeFromMap.call(this);
    },
    
    minZoomLevel: function() {
        return 1;
    },
    
    maxZoomLevel: function() {
        return 19;
    },
    
    /** redraw the map - if the layer is invisible, ignore the call, otherwise refresh the view */
    redraw: function() {
        if (!this.map()) {
            return;
        }
        if (this.visible()) {
            this.getShapeLayer().Show();
        } else {
            this.getShapeLayer().Hide();
        }
    },

    getShapeLayer: function() {
        if (this.shapeLayer == null) {
            this.shapeLayer = new VEShapeLayer();
            this.map().AddShapeLayer(this.shapeLayer);
        }
        return this.shapeLayer;
    },
    
    destroyShapeLayer: function() {
        this.map().DeleteShapeLayer(this.getShapeLayer());
        this.shapeLayer = null;
    },
    
    reload: function() {
        this.stores.each(function(store) {
            store.reload();
        }, this);
    },
    
    // cache all existing virtual earth shapes
    beforeShapeStoreLoad: function(store, options) {
        this.fireEvent('beforedataload', 'Loading...');
    },
    
    refreshShape: function(store, record, operation) {
        // if the layer is persisted, changes to the datastore will trigger a reload from the server negating the need to refresh shapes before the load
        if (typeof(operation) != "undefined" && (operation != Ext.data.Record.EDIT || this.isPersisted())) {
            return null; // only refresh shapes on commit
        }
        
        var layer = this.getShapeLayer();

        // try the shapeCache
        var shape = this.getShapeByRecord(record);
        if (shape == null) { // create a new veshape
            shape = this.createNewVeShape(record);
            layer.AddShape(shape);
            if (TNRIS.SelectionManager.isSelectedRecord(this, record)) {
                this.mapPanel().applySelectedShapeStyling(shape);
            } else {
                this.applyShapeStyling(shape, record);
            }
            this.cacheShape(record, shape);
        } 

        if (shape != null) {
            shape.SetDescription(this.getShapeDescription(record));
        }
        return shape;
    },

    refresh: function() {
        var layer = this.getShapeLayer();
        var currentShapeIDs = [];
        for(i=0; i < layer.GetShapeCount(); i++) {
            currentShapeIDs.push(layer.GetShapeByIndex(i).GetID());
        }
        
        var store = this.getShapeStore();
        store.each(function(record) {
            var shape = this.refreshShape(store, record);
            shape && currentShapeIDs.remove(shape.GetID());
        }, this);

        // Delete any pre-existing shape that wasn't found in a record or the shape cache
        Ext.each(currentShapeIDs, function(shapeId) {
            this.uncacheShape(shapeId);
            layer.DeleteShape(layer.GetShapeByID(shapeId));
        }, this);
    },

    applyShapeStyling: function (shape, record) {
        shape.SetFillColor(this.getShapeFillColor());
        shape.SetLineColor(this.getShapeLineColor());
        if (shape.GetType() != VEShapeType.Pushpin) {
            shape.SetCustomIcon('<div style="width:20px; height:20px"> </div>');
            // shape.HideIcon();
        } else {
            shape.SetCustomIcon(this.getShapeIcon());
        }
    },
    
    getShapeDescription: function(record) {
        var description = "";
        if (this.columnConfigs()) {
            this.sortColumnConfig();
            Ext.each(this.columnConfigs(), function(config) {
               if (! config.isHidden) {
                    description = description + String.format('<div class="nameValuePair">{0}</div>', this.getShapeDescriptionField(config,record));
               }
            }, this);
        } else {
            $H(record.data).each(function(entry) {
                if (entry.key != "id" && entry.key != "shapeType" && entry.key != "layerId" && entry.key != "shapePoints" && entry.key != "name" && entry.key != "shapeId" && entry.value != null) {
                    description = description + '<div class="nameValuePair">' +'<span class="nameColumn">'+ entry.key + ':</span><span class="valueColumn">' + entry.value +'</span></div>';
                }
                if (entry.key == "name") {
                    description = description + '<div class="nameValuePair">' + '<span class="nameTitleColumn">' + 'Name:</span><span class="valueTitleColumn">'  + entry.value + '</span></div>';
                }
            }.bind(this));
        }
        
        description = description + this.getLinkButtonDescription(record);
        return description;
    },
      
    getShapeDescriptionField: function(config, record) {
        return ('<span class="nameColumn">'+config.guiName + ':</span><span class="valueColumn">' + record.get(config.dbName) +'</span>');
    },
    
    getLinkButtonDescription: function(record) {
        return "";
    },
    
    sortColumnConfig: function() {
        this.columnConfigs().sort(function(obj1,obj2) { //sort by order
            if (obj1.order > obj2.order) { return 1; }
            else if (obj1.order < obj2.order) { return -1;}
            return 0;
        });
    },

    /******************************
    * Data Functions
    ******************************/
    
    getRecordConstructor: function(createNew) {
        if (createNew) {
            var fields = [
                {name: 'shapeType', mapping: 'shapeType'}, 
                {name: 'shapePoints', mapping: 'shapePoints'}
            ];
            return Ext.data.Record.create(fields);
        } else {
            return this.getShapeStore().recordType;
        }
    },
    
    clearRecordConstructor: function() {
        this.recordConstructor = null;
    },
    
    createShapeStore: function(context) {
        var shapeReader = new Ext.data.ArrayReader({
                id: 0                     // The subscript within row Array that provides an ID for the Record (optional)
                }, this.getRecordConstructor(true));
        var shapeProxy = new TNRIS.FunctionProxy({findAll: this.getUSPName(context + '-findAll'), findBy: this.getUSPName(context + '-findBy') });
        var store = new Ext.data.Store({
            proxy: shapeProxy,
            reader: shapeReader,
            baseParams: this.getBaseParams(),
            pruneModifiedRecords: true,
            autoLoad: false,
            listeners: {
                'beforeload': {scope: this, fn: function(store, options) { store.baseParams = this.getBaseParams(); }}
            }
        });
        shapeProxy.store = store;
        if (this.synchronizeStores() && this.stores.getCount() > 0 && !this.isPersisted()) {
            store.add(this.stores.first().getRange());
        }

        this.stores.add(context, store);
        return store;
    },
    
    removeStore: function(store) {
        this.stores.remove(store);
        store.purgeListeners();
        Ext.StoreMgr.unregister(store);
    },
    
    getStore: function(key) {
        return this.stores.get(key);
    },
    
    commitAllStores: function() {
        this.stores.each(function(store) {
            store.commitChanges();
        }, this);
    },
    
    getShapeStore: function() {
        if (this.getStore('map') == null) {
            var store = this.createShapeStore('map');
            store.on({
                'beforeload': {
                    scope: this,
                    fn: this.beforeShapeStoreLoad
                },
                'load': {
                    scope: this,
                    fn: function(store, records, options) {
                        this.fireEvent('dataload');
                    }
                },
                'loadException': {
                    scope: this,
                    fn: function(store,data,arg,e) {
                        this.fireEvent('loadexception');
                    }
                }
            });
        }
        return this.getStore('map');
    },
    
    loadShapeData: function(gridData) {
        if (this.stores.length == 0) {
            this.getShapeStore();
        }
        this.stores.each(function(store) {
            var reader = store.reader;
            store.reader = new Ext.data.JsonReader(gridData.metadata, Ext.data.Record.create(['shape', 'shapeType']));
            store.loadData(gridData);
            store.reader = reader;
        }, this);
    },
    
    /** whether to make all map and grid stores synchronize current data */
    synchronizeStores: function() {
        return false;
    },
    
    getShapeByRecord: function(record) {
        return this.getShapeByRecordId(record.id);
    },
    
    getShapeByRecordId: function(recordId) {
        var item = this.cachedVEShapes.get(recordId);
        if (null != item) {
            return this.getShape(item.shapeId);
        } else {
            return null;
        }
    },
    
    getRecordIdByShape: function(_shape) {
        var shape = typeof(_shape) == "string" ? _shape : _shape.GetID();
        var itemIdx = this.cachedVEShapes.findIndex('shapeId', shape);
        return itemIdx != -1 ? this.cachedVEShapes.itemAt(itemIdx).record : null;
    },
    
    getRecordByShape: function(_shape) {
        var id = this.getRecordIdByShape(_shape);
        if (id != null) {
            return this.getShapeStore().getById(id);
        } else {
            return null;
        }
    },
    
    getRecord: function(recordId) {
        var record = null;
        this.stores.each(function(store) {
            record = store.getById(record.id);
            return record != null;
        }, this);
        return record;
    },
    
    cacheShape: function(record, _shape) {
        var shape = this.getShape(_shape);
        this.cachedVEShapes.add(record.id, {shapeId: shape.GetID(), record: record.id});
    },
    
    uncacheShape: function(_shape) {
        this.cachedVEShapes.removeKey(this.getRecordIdByShape(_shape));
    },
    
    uncacheRecord: function(record) {
        this.cachedVEShapes.removeKey(record.id);
    },
    
    /** find the VEShape for a given shape record
    @param {Ext.data.Record} record - must have a shapeId field or null is returned
    @returns {VEShape} 
    */
    getShape: function(shape) {
        if (typeof(shape) == "string") {
            return this.map().GetShapeByID(shape);
        } else {
            return shape;
        }
    },
    
    getUSPName: function(context) {
        if (context.indexOf('map') != -1) {
            return 'findClusteredSpatialRecords';
        } else {
            return 'findSpatialRecords';
        }
    },
    
    load: function(map) {
        var viewRect = map.GetMapView();
        this.getShapeStore().load({params: {envelope: [viewRect.TopLeftLatLong.Latitude, viewRect.TopLeftLatLong.Longitude, viewRect.BottomRightLatLong.Latitude, viewRect.BottomRightLatLong.Longitude]}});
    },
    
    getBaseParams: function() {
        return {layerId: this.layerId()};
    },
    
    getVirtualEarthUrl: function() {
        return this.defaultMethod('virtual_earth_url');
    },
    
    /************************************
    * Virtual Earth to Geography Functions
    *************************************/
    footprintAsLongRectangle: function() {
        return this.shapeLayer.GetBoundingRectangle();
    },
    
    footprintAsPointList: function() {
        return [new VELatLong(35, -104),new VELatLong(25, -104),new VELatLong(25, -95),new VELatLong(35, -95)];
    },
    
    /**
    Create a Virtual Earth Shape from primitives
    @param {String} type the type of shape (polygon, polyline, or point)
    @param {Array} pointArray array of VELatLong objects <b>or</b> 2 element arrays [lat long]
    @returns {VEShape}
    */
    createNewVeShape: function(record) {
       return this.veShapeFromPoints(record.get('shapeType'), record.get('shapePoints'));
    },

    veShapeFromPoints: function(type, pointArray) {
        var latLongs = [];
        var veType;
        switch (type.toLowerCase()) {
            case "polygon": veType = VEShapeType.Polygon; break;
            case "polyline": veType = VEShapeType.Polyline; break;
            case "point": veType = VEShapeType.Pushpin; break;
            default: throw new Exception("Unsupported Shape Type " + type + ".  Unable to draw shape on map");
        }
        for(var i = 0; i < pointArray.length; i++) {
            var latLong = pointArray[i];
            if (latLong.Latitude == null) {
                latLong = new VELatLong(pointArray[i][0], pointArray[i][1]);
            }
            latLongs.push(latLong);
        }
        return new VEShape(veType, latLongs);
    },
    
    /*****************************
    * Grid Function
    *****************************/
    removeFromGrid: function() {
        this.removeStore(getGridStore());
    },
    
    getGridStore: function() {
        if (this.getStore('grid') == null) {
            var store = this.createShapeStore('grid');
            store.on({
                'load': {
                    scope: this,
                    fn: function(store, records, options) {
                        this.fireEvent('loadGrid', store, records, options);
                    }
                },
                'loadException': {
                    scope: this,
                    fn: function(store,data,arg,e) {
                        this.fireEvent('loadexception');
                    }
                }
            });
        }
        return this.getStore('grid');
    },
    
    loadGridData: function() {
        var store = this.getGridStore();
        if (this.isPersisted()) {
            if (store.getCount() > 0) {
                this.fireEvent('loadGrid', store, [], {});
            } else {
                this.getGridStore().load({start: 1, limit: 20});
            }
        } else {
            this.fireEvent('loadGrid', store, [], {});
        }
    },
    
    /** return colum specs for the layer's grid data */
    gridColumnSpecs: function() {
        var gridStore = this.getGridStore();
        if (null == gridStore || null == gridStore.fields) {
            return [];
        }
        
        var fields = [];
        if (this.columnConfigs() != null) {
            this.sortColumnConfig();
            Ext.each(this.columnConfigs(), function(config) {
                if (! config.isHidden) {
                    fields.push( {header: config.guiName, dataIndex: config.dbName});
                }
            }, this);
        } else {
            for (var i = 0; i < gridStore.fields.keys.length; i++ ) {
                var field = gridStore.fields.keys[i];
                switch(field) {
                    case 'id':
                    case 'shapeType':
                    case 'shapePoints':
                        break;
                    default:
                        fields.push( { header: field, dataIndex: field});
                        break;
                }
            }
        }
        return fields;
    },
    
    /**********************************
    * Accessors
    **********************************/
    
    getShapeLineColor: function(value) {
        return this.shapeLineColor;
    },
    
    getShapeFillColor: function(value) {
        return this.shapeFillColor;
    },
    
    getShapeIcon: function(value) {
        return this.shapeIcon;
    }

});

/** @class Queries a ShapeLayer to limit the results drawn on the map or in a grid.  Currently limited to a single layer
@augments TNRIS.ShapeLayer
@param {configuration} config
@config {TNRIS.LayerBase} layer the layer on which this smart layer is based
@config {Array} params an array of objects defining the query:
    [{ 
    field: {column name}
    operator: 'equals' || 'greaterThan' || 'lessThan'
    value: {value to compare against}
    }]
*/
TNRIS.SmartLayer = function(config) {
    Ext.apply(this, config);
    TNRIS.SmartLayer.superclass.constructor.call(this, {});
};

Ext.extend(TNRIS.SmartLayer, TNRIS.ShapeLayer, 
    /** @scope TNRIS.SmartLayer */
    {
    /** True if data should be loaded from the server when the layer is first added */
    doLoadOnAdd: function() {
        return true;
    },
    
    columnConfigs: function() {
        return this.layer.columnConfigs();
    },
    
    getGridStore: function() {
        return this.getShapeStore(); // both grid and map will have same data
    },
    
    loadGridData: function() {
        var store = this.getGridStore();
        this.fireEvent('loadGrid', store, [], {});
    },

    /** 
    builds a baseParam object based on the layer and params 
    */
    getBaseParams: function() {
        var bparams = {layerId: this.layer.layerId()};
        $A(this.params).each(function(entry) {
            var field = entry.field;
            switch (entry.operator) {
                case 'equals': /* do nothing */ break;
                case 'greaterThan': 
                    field = field + ">";
                    break;
                case 'lessThan':
                    field = field + "<";
                    break;
                default: /* do nothing */ break;
            }
            bparams[field] = entry.value;
        }.bind(this));
        return bparams;
    },
    
    /** overrides the baseclass to return an autogenerated ID if necessary */
    id: function(value) {
        if (typeof(value) == 'undefined') {
            var id = this.get('layer_id');
            if (id == null) {
                this.set('layer_id', Ext.layerId());
                return this.layerId();
            }
        } else {
            return this.set('layer_id', value);
        }
    },
    
    micronail: function(value) {
        return this.layer.micronail();
    },
    
    /** smart layers can't be made into another smart layer */
    smartable: function(value) {
        return false;
    },
    
    suggestSmartLayer: function() {
        return false;
    }
});

TNRIS.ImportLayer = function(config) {
    TNRIS.ImportLayer.superclass.constructor.call(this, config);
    
    Ext.applyIf(this.config, {
        zoomToLayerAfterLoad : false
    });
};

Ext.extend(TNRIS.ImportLayer, TNRIS.ShapeLayer, 
    /** @scope TNRIS.ImportLayer */
    {
    /** shapelayers are queryable by default */
    queryable: function(value) {
        return 0;
    },
    
    isEditable: function() {
        return true;
    },
    
    isRefreshable: function() {
        return true;
    },
    
    refreshTimer: function() {
        this.load();
    },
    
    /** shape layers can be made into smart layers */
    smartable: function(value) {
        return false;
    },
   
    isDisplayedDynamically: function() {
        // TODO - calculate this based on type?
        return false;
    },

    /** True if data should be loaded from the server when the layer is first added */
    doLoadOnAdd: function() {
        return true;
    },

    load: function() {
        if (this.config.source_url == null) {
            throw "ImportLayers must have a source defined.";
        }
        // TODO: determine VEDataType from config object
        this.fireEvent('beforedataload', "Loading...");
        try {
            var layer = this.getShapeLayer();
            layer.DeleteAllShapes();
            var spec = new VEShapeSourceSpecification(VEDataType.ImportXML, this.config.source_url, layer);
            this.map().ImportShapeLayerData(spec, this.onLoad.bind(this), this.zoomToLayerAfterLoad);
        } catch (e) {
            console.log("Unable to import kml: " + e.message);
        }
    },
    
    onLoad: function(mapGuid, error) {
        this.fireEvent('dataload');
        if (error) {
            Ext.Msg.alert("Error", "The following error occured while loading the kml url: " + error);
        }
        this.timestamp = Ext.util.Format.date(new Date(), 'm/d/y H:i:s');
        this.fireEvent('layer-refresh', {"time":this.timestamp});
    },
    
    refresh: function() {
    }
});

if (typeof(Sys) !== "undefined") { Sys.Application.notifyScriptLoaded(); }
