/*
 * Copyright (c) 2008-2015 The Open Source Geospatial Foundation
 *
 * Published under the BSD license.
 * See https://github.com/geoext/geoext2/blob/master/license.txt for the full
 * text of the license.
 */

/*
 * @requires GeoExt/data/LayerStore.js
 * @include OpenLayers/Map.js
 */

/**
 * Create a panel container for a map. The map contained by this panel
 * will initially be zoomed to either the center and zoom level configured
 * by the `center` and `zoom` configuration options, or the configured
 * `extent`, or - if neither are provided - the extent returned by the
 * map's `getExtent()` method.
 *
 * Example:
 *
 *     var mappanel = Ext.create('GeoExt.panel.Map', {
 *         title: 'A sample Map',
 *         map: {
 *             // ...
 *             // optional, can be either
 *             //   - a valid OpenLayers.Map configuration or
 *             //   - an instance of OpenLayers.Map
 *         },
 *         center: '12.31,51.48',
 *         zoom: 6
 *     });
 *
 * A Map created with code like above is then ready to use as any other panel.
 * To have a fullscreen map application, you could e.g. add it to a viewport:
 *
 * Example:
 *
 *     Ext.create('Ext.container.Viewport', {
 *         layout: 'fit',
 *         items: [
 *             mappanel // our variable from above
 *         ]
 *     });
 *
 * @class GeoExt.panel.Map
 */
Ext.define('GeoExt.panel.Map', {
    extend: 'Ext.panel.Panel',
    requires: [
        'Ext.layout.container.Fit',
        'GeoExt.data.LayerStore'
    ],
    alias: 'widget.gx_mappanel',
    alternateClassName: 'GeoExt.MapPanel',

    statics: {
        /**
         * The first map panel found via an the Ext.ComponentQuery.query
         * manager.
         *
         * Convenience function for guessing the map panel of an application.
         * This can reliably be used for all applications that just have one map
         * panel in the viewport.
         *
         * @return {GeoExt.panel.Map}
         * @static
         */
        guess : function() {
            var candidates = Ext.ComponentQuery.query("gx_mappanel");
            return ((candidates && candidates.length > 0)
                ? candidates[0]
                : null);
        }
    },

    /**
     * A location for the initial map center.  If an array is provided, the
     * first two items should represent x & y coordinates. If a string is
     * provided, it should consist of a x & y coordinate seperated by a
     * comma.
     *
     * @cfg {OpenLayers.LonLat/Number[]/String} center
     */
    center: null,

    /**
     * An initial zoom level for the map.
     *
     * @cfg {Number} zoom
     */
    zoom: null,

    /**
     * An initial extent for the map (used if center and zoom are not
     * provided.  If an array, the first four items should be minx, miny,
     * maxx, maxy.
     *
     * @cfg {OpenLayers.Bounds/Number[]} extent
     */
    extent: null,

    /**
     * Set this to true if you want pretty strings in the MapPanel's state
     * keys. More specifically, layer.name instead of layer.id will be used
     * in the state keys if this option is set to true. But in that case
     * you have to make sure you don't have two layers with the same name.
     * Defaults to false.
     *
     * @cfg {Boolean} prettyStateKeys
     */
    /**
     * Whether we want the state key to be pretty. See
     * {@link #cfg-prettyStateKeys the config option prettyStateKeys} for
     * details.
     *
     * @property {Boolean} prettyStateKeys
     */
    prettyStateKeys: false,

    /**
     * A configured map or a configuration object for the map constructor.
     *
     * In most cases you will want your map to be configured with
     * `fallThrough: true`, as other settings affect the dragging behaviour of
     * overlayed `Ext.window.Window` instances in negative way. Such windows
     * cannot be smoothly dragged over the the map panel. If you do not provide
     * a map or map configuration object, the auto-created map will be
     * configured with `fallThrough` being `true`.
     *
     * Having `fallThrough` being `false` is a misconfiguration most of the
     * time, which is why we will issue a warning to the developer console if we
     * detect this setting.
     *
     * A configured map will be available after construction through the
     * {@link GeoExt.panel.Map#property-map} property.
     *
     * @cfg {OpenLayers.Map/Object} map
     */
    /**
     * A map or map configuration.
     *
     * @property {OpenLayers.Map/Object} map
     */
    map: null,

    /**
     * In order for child items to be correctly sized and positioned,
     * typically a layout manager must be specified through the layout
     * configuration option.
     *
     * @cfg {OpenLayers.Map/Object} layout
     */
    /**
     * A layout or layout configuration.
     *
     * @property {OpenLayers.Map/Object} layout
     */
    layout: 'fit',

    /**
     * The layers provided here will be added to this Map's
     * {@link #property-map}.
     *
     * @cfg {GeoExt.data.LayerStore/OpenLayers.Layer[]} layers
     */
    /**
     * A store containing {@link GeoExt.data.LayerModel gx_layer-model}
     * instances.
     *
     * @property {GeoExt.data.LayerStore} layers
     */
    layers: null,

    /**
     * Array of state events.
     *
     * @property {String[]} stateEvents
     * @private
     */
    stateEvents: [
        "aftermapmove",
        "afterlayervisibilitychange",
        "afterlayeropacitychange",
        "afterlayerorderchange",
        "afterlayernamechange",
        "afterlayeradd",
        "afterlayerremove"
    ],

    /**
     * Whether we already rendered an OpenLayers.Map in this panel. Will be
     * updated in #onResize, after the first rendering happened.
     *
     * @property {Boolean} mapRendered
     * @private
     */
    mapRendered: false,

    /**
     * Initializes the map panel. Creates an OpenLayers map if
     * none was provided in the config options passed to the
     * constructor.
     *
     * Such an auto-created map will be configured with
     *
     *     {
     *         allOverlays: true,
     *         fallThrough: true
     *     }
     *
     * See {@link GeoExt.panel.Map#cfg-map} for an explanation why we do this.
     *
     * @private
     */
    initComponent: function(){
        if(!(this.map instanceof OpenLayers.Map)) {
            this.map = new OpenLayers.Map(
                Ext.applyIf(this.map || {}, {
                    allOverlays: true,
                    fallThrough: true
                })
            );
        }

        if (this.map.fallThrough !== true) {
            this.warnMapFallThrough();
        }

        var layers  = this.layers;
        if(!layers || layers instanceof Array) {
            this.layers = Ext.create('GeoExt.data.LayerStore', {
                layers: layers,
                map: this.map.layers.length > 0 ? this.map : null
            });
        }

        if (Ext.isString(this.center)) {
            this.center = OpenLayers.LonLat.fromString(this.center);
        } else if(Ext.isArray(this.center)) {
            this.center = new OpenLayers.LonLat(this.center[0], this.center[1]);
        }
        if (Ext.isString(this.extent)) {
            this.extent = OpenLayers.Bounds.fromString(this.extent);
        } else if(Ext.isArray(this.extent)) {
            this.extent = OpenLayers.Bounds.fromArray(this.extent);
        }

        this.callParent(arguments);

        // The map is renderer and its size is updated when we receive
        // "resize" events.
        this.on('resize', this.onResize, this);

        //TODO This should be handled by a LayoutManager
        this.on("afterlayout", function() {
            //TODO remove function check when we require OpenLayers > 2.11
            if (typeof this.map.getViewport === "function") {
                this.items.each(function(cmp) {
                    if (typeof cmp.addToMapPanel === "function") {
                        cmp.getEl().appendTo(this.body);
                    }
                }, this);
            }
        }, this);

        /**
         * Fires after the map is moved.
         *
         * @event aftermapmove
         */
        /**
         * Fires after a layer changed visibility.
         *
         * @event afterlayervisibilitychange
         */
        /**
         * Fires after a layer changed opacity.
         *
         * @event afterlayeropacitychange
         */
        /**
         * Fires after a layer order changed.
         *
         * @event afterlayerorderchange
         */
        /**
         * Fires after a layer name changed.
         *
         * @event afterlayernamechange
         */
        /**
         * Fires after a layer added to the map.
         *
         * @event afterlayeradd
         */
        /**
         * Fires after a layer removed from the map.
         *
         * @event afterlayerremove
         */

        // bind various listeners to the corresponding OpenLayers.Map-events
        this.map.events.on({
            "moveend": this.onMoveend,
            "changelayer": this.onChangelayer,
            "addlayer": this.onAddlayer,
            "removelayer": this.onRemovelayer,
            scope: this
        });
    },

    /**
     * Logs a warning to the console (if one is present) that tells the user to
     * set the `fallThrough` property of an OpenLayers.Map to true when this map
     * is being used inside of a GeoExt.panel.Map.
     *
     * @private
     */
    warnMapFallThrough: function(){
        Ext.log({
            level: 'warn',
            msg: 'It is recommended to construct a GeoExt.panel.Map with' +
                ' OpenLayers.Map#fallThrough == true. This way dragging' +
                ' interactions with floating components (e.g.' +
                ' Ext.window.Window) on top of the map are smoother.'
        });
    },

    /**
     * The "moveend" listener bound to the
     * {@link GeoExt.panel.Map#property-map}.
     *
     * @param {Object} e
     * @private
     */
    onMoveend: function(e) {
        this.fireEvent("aftermapmove", this, this.map, e);
    },

    /**
     * The "changelayer" listener bound to the
     * {@link GeoExt.panel.Map#property-map}.
     *
     * @param {Object} e
     * @private
     */
    onChangelayer: function(e) {
        var map = this.map;
        if (e.property) {
            if (e.property === "visibility") {
                this.fireEvent("afterlayervisibilitychange", this, map, e);
            } else if (e.property === "order") {
                this.fireEvent("afterlayerorderchange", this, map, e);
            } else if (e.property === "name") {
                this.fireEvent("afterlayernamechange", this, map, e);
            } else if (e.property === "opacity") {
                this.fireEvent("afterlayeropacitychange", this, map, e);
            }
        }
    },

    /**
     * The "addlayer" listener bound to the
     * {@link GeoExt.panel.Map#property-map}.
     *
     * @param {Object} e
     * @private
     */
    onAddlayer: function() {
        this.fireEvent("afterlayeradd");
    },

    /**
     * The "removelayer" listener bound to the
     * {@link GeoExt.panel.Map#property-map}.
     *
     * @param {Object} e
     * @private
     */
    onRemovelayer: function() {
        this.fireEvent("afterlayerremove");
    },

    /**
     * Private method called after the panel has been rendered or after it
     * has been laid out by its parent's layout.
     *
     * @private
     */
    onResize: function() {
        var map = this.map;
        if(!this.mapRendered && this.body.dom !== map.div) {
            // the map has not been rendered yet
            map.render(this.body.dom);
            this.mapRendered = true;

            this.layers.bindMap(map);

            if (map.layers.length > 0) {
                this.setInitialExtent();
            } else {
                this.layers.on("add", this.setInitialExtent, this,
                               {single: true});
            }
        } else {
            map.updateSize();
        }
    },

    /**
     * Set the initial extent of this panel's map.
     *
     * @private
     */
    setInitialExtent: function() {
        var map = this.map;
        if (!map.getCenter()) {
            if (this.center || this.zoom ) {
                // center and/or zoom?
                map.setCenter(this.center, this.zoom);
            } else if (this.extent instanceof OpenLayers.Bounds) {
                // extent
                map.zoomToExtent(this.extent, true);
            }else {
                map.zoomToMaxExtent();
            }
        }
    },

    /**
     * Returns a state of the Map as keyed Object. Depending on the point in
     * time this method is being called, the following keys will be available:
     *
     * * `x`
     * * `y`
     * * `zoom`
     *
     * And for all layers present in the map the object will contain the
     * following keys
     *
     * * `visibility_<XXX>`
     * * `opacity_<XXX>`
     *
     * The &lt;XXX&gt; suffix is either the title or id of the layer record, it
     * can be influenced by setting #prettyStateKeys to `true` or `false`.
     *
     * @return {Object}
     * @private
     */
    getState: function() {
        var me = this,
            map = me.map,
            state = me.callParent(arguments) || {},
            layer;

        // Ext delays the call to getState when a state event
        // occurs, so the MapPanel may have been destroyed
        // between the time the event occurred and the time
        // getState is called
        if(!map) {
            return;
        }

        // record location and zoom level
        var center = map.getCenter();
        // map may not be centered yet, because it may still have zero
        // dimensions or no layers
        center && Ext.applyIf(state, {
            "x": center.lon,
            "y": center.lat,
            "zoom": map.getZoom()
        });

        me.layers.each(function(modelInstance) {
            layer = modelInstance.getLayer();
            layerId = this.prettyStateKeys
                   ? modelInstance.get('title')
                   : modelInstance.get('id');
            state = me.addPropertyToState(state, "visibility_" + layerId,
                layer.getVisibility());
            state = me.addPropertyToState(state, "opacity_" + layerId,
                (layer.opacity === null) ? 1 : layer.opacity);
        }, me);

        return state;
    },

    /**
     * Apply the state provided as an argument.
     *
     * @param {Object} state The state to apply.
     * @private
     */
    applyState: function(state) {
        var me = this;
            map = me.map;
        // if we get strings for state.x, state.y or state.zoom
        // OpenLayers will take care of converting them to the
        // appropriate types so we don't bother with that
        me.center = new OpenLayers.LonLat(state.x, state.y);
        me.zoom = state.zoom;

        // set layer visibility and opacity
        var layer, layerId, visibility, opacity;
        me.layers.each(function(layerRec) {
            layer = layerRec.getLayer();
            layerId = me.prettyStateKeys ? layer.name : layer.id;
            visibility = state["visibility_" + layerId];
            if(visibility !== undefined) {
                // convert to boolean
                visibility = (/^true$/i).test(visibility);
                if(layer.isBaseLayer) {
                    if(visibility) {
                        map.setBaseLayer(layer);
                    }
                } else {
                    layer.setVisibility(visibility);
                }
            }
            opacity = state["opacity_" + layerId];
            if(opacity !== undefined) {
                layer.setOpacity(opacity);
            }
        });
    },

    /**
     * Check if an added item has to take separate actions
     * to be added to the map.
     * See e.g. the GeoExt.slider.Zoom or GeoExt.slider.LayerOpacity
     *
     * @private
     */
    onBeforeAdd: function(item) {
        if(Ext.isFunction(item.addToMapPanel)) {
            item.addToMapPanel(this);
        }
        this.callParent(arguments);
    },

    /**
     * Private method called during the destroy sequence.
     *
     * @private
     */
    beforeDestroy: function() {
        if(this.map && this.map.events) {
            this.map.events.un({
                "moveend": this.onMoveend,
                "changelayer": this.onChangelayer,
                scope: this
            });
        }
        // if the map panel was passed a map instance, this map instance
        // is under the user's responsibility
        if(!this.initialConfig.map ||
           !(this.initialConfig.map instanceof OpenLayers.Map)) {
            // we created the map, we destroy it
            if(this.map && this.map.destroy) {
                this.map.destroy();
            }
        }
        delete this.map;
        this.callParent(arguments);
    }
});