/**
 * Horde mapping service.  This file provides a general API for interacting with
 * inline "slippy" maps. You must also include the file for the specific
 * provider support you want included.
 *
 * Copyright 2009-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.horde.org/licenses/gpl.
 *
 * @author Michael J. Rubinsky <mrubinsk@horde.org>
 */

/**
 * Class for dealing with OpenLayers based inline mapping.
 *
 * Requires that the openlayer.js file has been included, as well as any js files
 * specific to any commercial mapping providers, such as google/bing/osm etc...
 *
 * var options = {};
 * // opts.defaultBase      - Id of the baselayer to enable by default.
 * // opts.delayed          - Don't bind the map to the dom until display() is
 * //                         called.
 * // opts.elt              - DOM Node to place map in
 * // opts.onhover          - Event handler to run on feature hover (hightlight)
 * // opts.layers           - An Array of OpenLayers.Layer objects
 * // opts.mapClick         - Callback to handle a click on the map
 * // opts.markerBackground - Custom marker background image to use by default.
 * // opts.markerDragEnd    - Callback to handle when a marker is dragged.
 * // opts.markerImage      - Custom marker image to use by default.
 * // opts.onClick          - Callback for handling click events on features.
 * // opts.onHover          - Callback for handling hover events on features.
 * // opts.panzoom          - Use the larger PanZoomBar control. If false, will
 * //                         use the smaller ZoomPanel control.
 * //                       - Callback
 * // opts.useMarkerLayer   - Add a vector layer to be used to place markers.
 * // opts.hide             - Don't show markerlayer in LayerSwitcher
 * // opts.onBaseLayerChange - Callback fired when baselayer is changed.
 * // opts.zoomworldicon    - Show the worldzoomicon on the PanZoomBar control
 * //                         that resets/centers map.
 * var map = new HordeMap.OpenLayers(options);
 *
 */
HordeMap.Map.Horde = Class.create({

    map: null,
    markerLayer: null,
    _proj: null,
    _layerSwitcher: null,

    initialize: function(opts)
    {
        // @TODO: BC Break
        if (HordeMap.conf.markerImage) {
            opts.markerImage = HordeMap.conf.markerImage;
            opts.markerBackground = HordeMap.conf.markerBackground;
        }

        // defaults
        var o = {
            useMarkerLayer: false,
            draggableFeatures: true,
            showLayerSwitcher: true,
            markerLayerTitle: 'Markers',
            delayed: false,
            panzoom: true,
            zoomworldicon: false,
            layers: [],
            onHover: false,
            onClick: false,
            hide: true,
            onBaseLayerChange: false,
            defaultBaseLayer: false,
            // default stylemap
            styleMap: new OpenLayers.StyleMap({
                'default': {
                    externalGraphic: opts.markerImage,
                    backgroundGraphic: opts.markerBackground,
                    backgroundXOffset: 0,
                    backgroundYOffset: -7,
                    backgroundGraphicZIndex: 10,
                    pointRadius: (opts.pointRadius) ? opts.pointRadius : 10
                }
            })
        };
        this.opts = Object.extend(o, opts || {});

        // Generate the base map object. Always use EPSG:4326 (WGS84) for display
        // and EPSG:900913 (spherical mercator) for projection for compatibility
        // with commercial mapping services such as Google etc...
        var options = {
            projection: new OpenLayers.Projection("EPSG:900913"),
            displayProjection: new OpenLayers.Projection("EPSG:4326"),
            units: "m",
            numZoomLevels: 18,
            maxResolution: 156543.0339,
            maxExtent: new OpenLayers.Bounds(-20037508, -20037508, 20037508, 20037508.34),
            controls: [
                new OpenLayers.Control.Navigation(),
                new OpenLayers.Control.Attribution()
            ],
            styleMap: this.opts.styleMap
        };
        if (this.opts.panzoom) {
            options.controls.push(new OpenLayers.Control.PanZoomBar({ 'zoomWorldIcon': this.opts.zoomworldicon }));
        } else {
            options.controls.push(new OpenLayers.Control.ZoomPanel());
        }
        // Set the language
        OpenLayers.Lang.setCode(HordeMap.conf.language);
        this.map = new OpenLayers.Map((this.opts.delayed ? null : this.opts.elt), options);

        // Create the vector layer for markers if requested.
        // @TODO H5 BC break - useMarkerLayer should be permap, not per page
        if (this.opts.useMarkerLayer || HordeMap.conf.useMarkerLayer) {
            this.markerLayer = this.createVectorLayer(this.opts);
            this.opts.layers.push(this.markerLayer);
        }

        this.map.addLayers(this.opts.layers);
        if (this.opts.showLayerSwitcher) {
            this._layerSwitcher = new OpenLayers.Control.LayerSwitcher();
            this.map.addControl(this._layerSwitcher);
        }

        // Create a click control to handle click events on the map
        if (this.opts.mapClick) {
            var click = new OpenLayers.Control.Click({
                onClick: this._onMapClick.bind(this)
            });
            this.map.addControl(click);
            click.activate();
        }

        // Used for converting between internal and display projections.
        this._proj = new OpenLayers.Projection("EPSG:4326");
        if (this.opts.defaultBaseLayer) {
           this.map.setBaseLayer(this.map.getLayersByName(this.opts.defaultBaseLayer).pop());
        }
        this.map.zoomToMaxExtent();
        if (this.opts.onBaseLayerChange) {
            this.map.events.register('changebaselayer', null, this.opts.onBaseLayerChange);
        }
    },

    /**
     * Create a vector layer and attach to map. Can pass hover and click
     * handlers if this is the *only* layer to use them. Otherwise, use
     * addHighlightControl/addClickControl methods after all layers are
     * created.
     *
     * opts
     *   markerLayerTitle  - The title to show in the LayerSwitcher
     *   hide              - Do not show layer in LayerSwitcher
     *   onHover           - Hover handler
     *   onClick           - Click handler
     */
    createVectorLayer: function(opts)
    {
        var styleMap = opts.styleMap || this.styleMap;
        var layer = new OpenLayers.Layer.Vector(
            opts.markerLayerTitle,
            {
                'styleMap': styleMap,
                'rendererOptions': { zIndexing: true }
            }
        );
        if (opts.hide) {
            layer.displayInLayerSwitcher = false;
        }
        if (opts.draggableFeatures) {
            var dragControl = new OpenLayers.Control.DragFeature(
                layer,
                { onComplete: opts.markerDragEnd });

            this.map.addControl(dragControl);
            dragControl.activate();
        }

        if (opts.onHover) {
            this.addHighlightControl({
                'onHover': opts.onHover,
                'layers': layer
            });
        }

        if (opts.onClick) {
            this.addClickControl({
                'layers': layer,
                'onClick': opts.onClick
            });
        }

        return layer;
    },

    addHighlightControl: function(opts)
    {
        var selectControl = new OpenLayers.Control.SelectFeature(
            opts.layers, {
                hover: true,
                highlightOnly: true,
                renderIntent: 'temporary',
                eventListeners: {
                     beforefeaturehighlighted: opts.onHover,
                     featurehighlighted: opts.onHover,
                     featureunhighlighted: opts.onHover
                }
            }
        );
        this.map.addControl(selectControl);
        selectControl.activate();

        return selectControl;
    },

    /**
     * Add a click control to the map. HordeMap only supports one selectFeature
     * control for click handlers per map, though it may contain several layers.
     *
     * @param object opts
     *    'layers': [] All layers that should be included in the control layer.
     *              Note that any layers on top of layers that should handle
     *              clicks *must* be included in the array.
     *              This is an OL requirement.
     *    'active': [] Layers that should actually respond to the click request.
     */
    addClickControl: function(opts)
    {
        var clickControl = new OpenLayers.Control.SelectFeature(
            opts.layers, {
                'hover': false,
                'clickout': false,
                'toggle': true,
                'hover': false,
                'multiple': false,
                'renderIntent': 'temporary'
            }
        );
        opts.active.each(function(l) {
            l.events.on({
                'featureselected': opts.onClick
            });
       });
        this.map.addControl(clickControl);
        clickControl.activate();

        return clickControl;
    },

    /**
     * @param string name  The feed display name.
     * @param string feed_url  The URL for the feed.
     * @param string proxy     A local proxy to get around same origin policy.
     *
     * @return OpenLayers.Layer  With narkers added to displayed map for each
     *                           georss entry.
     */
    addGeoRssLayer: function(name, feed_url, proxy)
    {
        var style = new OpenLayers.Style({ 'pointRadius': 20, 'externalGraphic': '${thumbnail}' });
        var layer = new OpenLayers.Layer.GML(name, feed_url, {
            projection: new OpenLayers.Projection('EPSG:4326'),
            format: OpenLayers.Format.GeoRSS,
            formatOptions: {
                createFeatureFromItem: function(item) {
                    var feature = OpenLayers.Format.GeoRSS.prototype
                        .createFeatureFromItem.apply(this, arguments);
                    feature.attributes.thumbnail =
                        this.getElementsByTagNameNS(
                            item, '*', 'thumbnail')[0].getAttribute('url');
                    return feature;
                }
            },
            styleMap: new OpenLayers.StyleMap({
                'default': style
            })
        });
        this.map.addLayer(layer);
        return layer;
    },

    removeGeoRssLayer: function(layer)
    {
        this.map.removeLayer(layer);
    },

    getZoom: function()
    {
        return this.map.getZoom();
    },

    display: function(n)
    {
        if (Object.isUndefined(this.map)) {
            return;
        }
        if (!n) {
            n = this.opts.elt;
        }
        this.map.render(n);
    },

    destroy: function()
    {
        this.map.destroy();
    },

    setCenter: function(p, z)
    {
        var ll = new OpenLayers.LonLat(p.lon, p.lat);
        ll.transform(this._proj, this.map.getProjectionObject());
        this.map.setCenter(ll, z);
    },

    zoomTo: function(z)
    {
        this.map.zoomTo(z);
    },

    /**
     * Adds a simple marker to the map. Will use the markerImage property
     * optionally passed into the map options. To add a feature with varying
     * markerImage, pass a stylecallback method that returns a suitable style
     * object.
     *
     * @param lonlat p    { 'lon': x, 'lat': y }
     * @para object opts  Options
     *    'styleCallback': callback to provide a custom styleobject for marker
     *    'layer': use this layer instead of this.markerLayer to place marker
     */
    addMarker: function(p, opts)
    {
        opts = Object.extend({ 'styleCallback': Prototype.K }, opts);
        var ll = new OpenLayers.Geometry.Point(p.lon, p.lat);
        ll.transform(this._proj, this.map.getProjectionObject());
        s = opts.styleCallback(this.markerLayer.style);
        var m = new OpenLayers.Feature.Vector(ll);
        if (opts.layer) {
            opts.layer.addFeatures([m]);
        } else {
            this.markerLayer.addFeatures([m]);
        }
        return m;
    },

    removeMarker: function(m, opts)
    {
        opts = opts || {};
        if (opts.layer) {
            opts.layer.destroyFeatures([m]);
        } else {
            this.markerLayer.destroyFeatures([m]);
        }
    },

    /**
     * Move a marker to new location.
     *
     * @param object m   An ol vector feature object representing the marker.
     * @param object ll  {lat: lon:}
     *
     * @return void
     */
    moveMarker: function(m, ll)
    {
        var point = new OpenLayers.LonLat(ll.lon, ll.lat);
        point.transform(this._proj, this.map.getProjectionObject());
        m.move(point);
    },

    /**
     * Zoom map to the best fit while containing all markers
     *
     */
    zoomToFit: function(layer)
    {
        if (!layer) {
            layer = this.markerLayer;
        }
        if (layer.getDataExtent()) {
            this.map.zoomToExtent(layer.getDataExtent());
        }
    },

    getMap: function()
    {
        return this.map;
    },

    updateMapSize: function()
    {
        this.map.updateSize();
        this.map.calculateBounds();
    },

    getMapNodeId: function()
    {
        return this.opts.elt;
    },

    _onFeatureDragEnd: function(feature)
    {
        if (this.opts.markerDragEnd) {
            return this.opts.markerDragEnd(feature);
        }
    },

    _onMapClick: function(e)
    {
        // get*Px functions always return units in the layer's projection
        var lonlat = this.map.getLonLatFromViewPortPx(e.xy);
        lonlat.transform(this.map.getProjectionObject(), this._proj);
        if (this.opts.mapClick) { this.opts.mapClick({ lonlat: lonlat }); }
    }

});

// Extension to OpenLayers to allow better abstraction:
OpenLayers.Feature.Vector.prototype.getLonLat = function() {
    var ll = new OpenLayers.LonLat(this.geometry.x, this.geometry.y);
    ll.transform(new OpenLayers.Projection('EPSG:900913'), new OpenLayers.Projection('EPSG:4326'));
    return ll;
};

// Custom OL click handler - doesn't propagate a click event when performing
// a double click
OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {
    defaultHandlerOptions: {
        'single': true,
        'double': false,
        'pixelTolerance': 0,
        'stopSingle': false,
        'stopDouble': false
    },

    initialize: function(options) {
        this.handlerOptions = OpenLayers.Util.extend({}, this.defaultHandlerOptions);
        OpenLayers.Control.prototype.initialize.apply(this, arguments);
        this.handler = new OpenLayers.Handler.Click(
            this, { 'click': options.onClick }, this.handlerOptions);
    }
});


HordeMap.Geocoder.Horde = Class.create({});
