//From John Resig: http://ejohn.org/blog/simple-javascript-inheritance/
(function(){var initializing=false,fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(prop){var _super=this.prototype;initializing=true;var prototype=new this();initializing=false;for(var name in prop){prototype[name]=typeof prop[name]=="function"&&typeof _super[name]=="function"&&fnTest.test(prop[name])?(function(name,fn){return function(){var tmp=this._super;this._super=_super[name];var ret=fn.apply(this,arguments);this._super=tmp;return ret;};})(name,prop[name]):prop[name];}function Class(){if(!initializing&&this.init)this.init.apply(this,arguments);}Class.prototype=prototype;Class.constructor=Class;Class.extend=arguments.callee;return Class;};})();
/*JSON.js*/
var JSON=function(){var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},s={'boolean':function(x){return String(x);},number:function(x){return isFinite(x)?String(x):'null';},string:function(x){if(/["\\\x00-\x1f]/.test(x)){x=x.replace(/([\x00-\x1f\\"])/g,function(a,b){var c=m[b];if(c){return c;}c=b.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);});}return'"'+x+'"';},object:function(x){if(x){var a=[],b,f,i,l,v;if(x instanceof Array){a[0]='[';l=x.length;for(i=0;i<l;i+=1){v=x[i];f=s[typeof v];if(f){v=f(v);if(typeof v=='string'){if(b){a[a.length]=',';}a[a.length]=v;b=true;}}}a[a.length]=']';}else if(x instanceof Date){function p(n){return n<10?'0'+n:n;};var tz=x.getTimezoneOffset();if(tz!=0){var tzh=Math.floor(Math.abs(tz)/60);var tzm=Math.abs(tz)%60;tz=(tz<0?'+':'-')+p(tzh)+':'+p(tzm);}else{tz='Z';}return'"'+x.getFullYear()+'-'+p(x.getMonth()+1)+'-'+p(x.getDate())+'T'+p(x.getHours())+':'+p(x.getMinutes())+':'+p(x.getSeconds())+tz+'"';}else if(x instanceof Object){a[0]='{';for(i in x){v=x[i];f=s[typeof v];if(f){v=f(v);if(typeof v=='string'){if(b){a[a.length]=',';}a.push(s.string(i),':',v);b=true;}}}a[a.length]='}';}else{return;}return a.join('');}return'null';}};return{copyright:'(c)2005 JSON.org',license:'http://www.crockford.com/JSON/license.html',stringify:function(v){var f=s[typeof v];if(f){v=f(v);if(typeof v=='string'){return v;}}return null;},eval:function(text){try{if(/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(text)){return eval('('+text+')');}}catch(e){}throw new SyntaxError("eval");},parse:function(text){var at=0;var ch=' ';function error(m){throw{name:'JSONError',message:m,at:at-1,text:text};}function next(){ch=text.charAt(at);at+=1;return ch;}function white(){while(ch){if(ch<=' '){next();}else if(ch=='/'){switch(next()){case'/':while(next()&&ch!='\n'&&ch!='\r'){}break;case'*':next();for(;;){if(ch){if(ch=='*'){if(next()=='/'){next();break;}}else{next();}}else{error("Unterminated comment");}}break;default:error("Syntax error");}}else{break;}}}function string(){var i,s='',t,u;if(ch=='"'){outer:while(next()){if(ch=='"'){next();return s;}else if(ch=='\\'){switch(next()){case'b':s+='\b';break;case'f':s+='\f';break;case'n':s+='\n';break;case'r':s+='\r';break;case't':s+='\t';break;case'u':u=0;for(i=0;i<4;i+=1){t=parseInt(next(),16);if(!isFinite(t)){break outer;}u=u*16+t;}s+=String.fromCharCode(u);break;default:s+=ch;}}else{s+=ch;}}}error("Bad string");}function array(){var a=[];if(ch=='['){next();white();if(ch==']'){next();return a;}while(ch){a.push(value());white();if(ch==']'){next();return a;}else if(ch!=','){break;}next();white();}}error("Bad array");}function object(){var k,o={};if(ch=='{'){next();white();if(ch=='}'){next();return o;}while(ch){k=string();white();if(ch!=':'){break;}next();o[k]=value();white();if(ch=='}'){next();return o;}else if(ch!=','){break;}next();white();}}error("Bad object");}function number(){var n='',v;if(ch=='-'){n='-';next();}while(ch>='0'&&ch<='9'){n+=ch;next();}if(ch=='.'){n+='.';while(next()&&ch>='0'&&ch<='9'){n+=ch;}}if(ch=='e'||ch=='E'){n+='e';next();if(ch=='-'||ch=='+'){n+=ch;next();}while(ch>='0'&&ch<='9'){n+=ch;next();}}v=+n;if(!isFinite(v)){}else{return v;}}function word(){switch(ch){case't':if(next()=='r'&&next()=='u'&&next()=='e'){next();return true;}break;case'f':if(next()=='a'&&next()=='l'&&next()=='s'&&next()=='e'){next();return false;}break;case'n':if(next()=='u'&&next()=='l'&&next()=='l'){next();return null;}break;}error("Syntax error");}function value(){white();switch(ch){case'{':return object();case'[':return array();case'"':return string();case'-':return number();default:return ch>='0'&&ch<='9'?number():word();}}return value();}};}();

(function($) {
  
  var PanZoomBar = OpenLayers.Class(OpenLayers.Control.PanZoomBar, {
    draw: function(px) {
        
       // initialize our internal div
        OpenLayers.Control.prototype.draw.apply(this, arguments);
        px = this.position.clone();

        // place the controls
        this.buttons = [];

        var sz = new OpenLayers.Size(18,18);
        var centered = new OpenLayers.Pixel(px.x+sz.w/2, px.y);
        var wposition = sz.w;

        if (this.zoomWorldIcon) {
            centered = new OpenLayers.Pixel(px.x+sz.w, px.y);
        }

        this._addButton("panup", "north-mini.png", centered, sz);
        px.y = centered.y+sz.h;
        px.x = centered.x-sz.w;
        this._addButton("panleft", "west-mini.png", px, sz);
        if (this.zoomWorldIcon) {
            this._addButton("zoomworld", "zoom-world-mini.png", px.add(sz.w, 0), sz);
            
            wposition *= 2;
        }
        this._addButton("panright", "east-mini.png", px.add(wposition*2, 0), sz);
        this._addButton("pandown", "south-mini.png", centered.add(0, sz.h*2), sz);
        this._addButton("zoomin", "zoom-plus-mini.png", centered.add(0, sz.h*3+5), sz);
        centered = this._addZoomBar(centered.add(0, sz.h*4 + 5));
        this._addButton("zoomout", "zoom-minus-mini.png", centered, sz);
        return this.div;
    }
  });
  
  /**
   * Performs synchronous server calls and returns the result as an object (array, number, etc.)
   * @private
   * @param {string} url The url to retrieve
   * @param {object} data (Optional) The data to pass either in the query string or the post string. Defaults to {}.
   * @param {string} method (Optional) The method to use, either 'GET' or 'POST' (not enforced). Defaults to 'POST'.
   */
  function sjson(url, data, method) {
    if( data === undefined ) { data = {}; }
    if( method === undefined ) { method = 'POST'; }
    var result = $.ajax({
      url: url,
      data: data,
      type: method,
      async: false
      });
    return JSON.parse(result.responseText);
  }
  
  var log_msgs = [];
  /**
   * Convenience logging method. Uses console.log if available. Otherwise puts value in an ever growing array.
   * @private
   * @param {object} msg The message to log. 
   */
  function log(msg) {
    if( console && console.log ) {
      console.log(msg);
    } else {
      log_msgs.push(msg);
    }
  }
  
  /**
   * Shows a simple modal.
   */
  function showModal(content, closeBtn) {
    var overlay = $('<div></div>');
    var modal = $('<div></div>');
    modal.html(content);
    
    //add a close button if requested
    if( closeBtn ) {
      var btn = $('<div>Close X</div>').css({clear: 'both', color: '#999', background: '#FFF', 'font-size': '18px', 'font-weight': 'bold', 'line-height': '20px', 'text-align': 'right', padding: '4px', cursor: 'pointer'});
      modal.prepend(btn);
    }
    
    overlay.click(function() { overlay.remove(); modal.remove(); });
    if( closeBtn ) {
      btn.click(function() { overlay.remove(); modal.remove(); });
    } else {
      modal.click(function() { overlay.remove(); modal.remove(); });
    }
    
    var max_height = Math.max($(document).height(), $('html').outerHeight(), $('body').outerHeight());
    var max_width = Math.max($(document).width(), $('html').outerWidth(), $('body').outerWidth());
    
    overlay.css({height: max_height, width: max_width, position: 'absolute', 'z-index': 1000, top: '0px', left:'0px', 'background-color': '#000', opacity: 0.3, margin: 0, padding: 0});
    modal.css({top: $(window).scrollTop() + (modal.height() > $(window).height() ? 0 : ($(window).height()-modal.height())/2), left: $(window).scrollLeft() + ($(window).width()-modal.width())/2, 'z-index': 1001, 'font-size': '90px', position: 'absolute', 'background-color': '#FFF', visibility: 'hidden'});

    $(window).scroll(function() { modal.css({top: $(window).scrollTop() + (modal.height() > $(window).height() ? 0 : ($(window).height()-modal.height())/2), left: $(window).scrollLeft() + ($(window).width()-modal.width())/2}); });
    
    $('body').append(overlay).append(modal);
    setTimeout(function() { modal.css({top: $(window).scrollTop() + (modal.height() > $(window).height() ? 0 : ($(window).height()-modal.height())/2), left: $(window).scrollLeft() + ($(window).width()-modal.width())/2, visibility: 'visible'}); }, 100);
  }
  
  /** @lends InteractiveMap# */
  this.InteractiveMap = Class.extend({
    defaultOptions: {
      display: 'DetailDisplay',
      width: 500,
      height: 500,
      marker_icon: 'img/marker.png',
      marker_icon_size: {width: 21, height: 25},
      marker_icon_offset: {left: -11, top: -25}
    },
    
    data_url: null,
    _container: null,
    _data: null,
    _detail_container: null,
    _map: null,
    _map_container: null,
    _map_controls: null, //holds the controls for the map.
    _marker_layer: null,
    isSmallMap: false,
    
    /** 
     * Creates a new InteractiveMap object
     * @class Creates an interactive map for harvest ministries
     * @constructs
     * @param {string} data_url The url the json data for this map can be found at.
     */
    init: function(data_url, options) {
      //only save the url if it was provided.
      if( data_url ) {
        this.data_url = data_url;
      }
      
      this.setOptions(options);
    },
    
    /**
     * Builds the options for this object based on the default options and the
     * passed in options
     */
    setOptions: function(options) {
      this.options = {};
      
      //add the default options
      for( var i in this.defaultOptions ) {
        this.options[i] = this.defaultOptions[i];
      }
      
      //add the passed in options
      for( i in options ) {
        this.options[i] = options[i];
      }
    },
    
    /**
     * Set the element that will contain the Widget
     * @param {String/Object} The selector for the object or the object itself
     *        that will contain the PagedBrowser
     * @note Accepts either a DOMElement or a jQuery object as well as a string
     *       that is a jQuery selector.
     */
    setContainer: function(obj) {
      if( typeof obj == 'string' ) {
        this._container = $(obj);
      } else if ( obj instanceof jQuery ) {
        this._container = obj;
      } else {
        this._container = $(obj);
      }
      this._container.css({width: this.options.width, height: this.options.height, position: 'relative'});
    },
    
    /**
     * Retrieve data from the server using the specified url.
     */
    getData: function() {
      if( this._data === null ) {
        //prevent stupidity
        if( this.data_url === null ) {
          throw new Error("You forgot to populate the data url.");
        }
        
        //do we assume that exceptions are the developer's fault?
        this._data = sjson(this.data_url);
      }
      
      return this._data;
    },
    
    /**
     * Creates the map object and begins to build the details of the map (layers, markers, etc.)
     */
    buildMap: function() {
      var data = this.getData();
      
      if( this._container === null ) {
        this._container = $('<div/>');
        this._container.appendTo('body');
        this._container.css({width: this.options.width, height: this.options.height, position: 'relative'});
      }
      
      if( this._map_container === null ) {
        this._map_container = $('<div/>');
        this._container.append(this._map_container);
        //this._map_container.css({position: 'absolute', top: 0, left: 0});
      }
      
      //set up default options for the map
      this._map_controls = [
        new OpenLayers.Control.Navigation(),
        new PanZoomBar()
      ];
      var options = {controls: this._map_controls};
      
      //add in user specified options
      if( data.options ) {
        $.extend(options, data.options);
        
        if( options.restrictedExtent ) {
          var bounds = options.restrictedExtent;
          options.restrictedExtent = new OpenLayers.Bounds(bounds.left, bounds.bottom, bounds.right, bounds.top);
        }
      }
      
      //create the map object.
      var map_div = $('<div/>').css({width: this.options.width, height: this.options.height});
      this._map_container.append(map_div);
      this._map = new OpenLayers.Map(map_div[0], options);
      
      //create the layers.
      this.createLayers(data.layers);
      
      //add markers to the map
      this.createMarkers(data.markers);
      
      //set the center and the zoom level
      this._map.zoomToMaxExtent();
    },
    
    /**
     * Creates the layers described and adds them to the _map.
     * @param {array} layers The layers to render (may be empty array)
     */
    createLayers: function(layers) {
      //TODO: rethink this, it may not really fit the needs.
      if( layers ) {
        
        for( var i = 0; i < layers.length; ++i ) {
          var bounds = layers[i].bounds;
          var size = layers[i].size;
          var resolution = layers[i].resolution;
          
          var options = {};
          if( resolution ) {
            if( resolution.min !== undefined && resolution.min !== null ) {
              options.minResolution = resolution.min;
            }
            if( resolution.max !== undefined && resolution.max !== null ) {
              options.maxResolution = resolution.max;
            }
          }
          
          var layer = new OpenLayers.Layer.Image(layers[i].name, layers[i].image, new OpenLayers.Bounds(bounds.left, bounds.bottom, bounds.right, bounds.top), new OpenLayers.Size(size.width, size.height), options);
          
          //add the layer to the map
          this._map.addLayer(layer);
        }
        
      }
    },
    
    createMarkers: function(markers) {
      var self = this;
      if( markers ) {
        //create the markers layer
        if( this._marker_layer === null ) {
          this._marker_layer = new OpenLayers.Layer.Markers("Markers");
          this._map.addLayer(this._marker_layer);
        }
        var layer = this._marker_layer;
        
        //create the marker icon
        var size = new OpenLayers.Size(this.options.marker_icon_size.width, this.options.marker_icon_size.height);
        var offset = new OpenLayers.Pixel(this.options.marker_icon_offset.left, this.options.marker_icon_offset.top);
        var icon = new OpenLayers.Icon(this.options.marker_icon, size, offset);
        
        //make all of the markers
        for( var i = 0; i < markers.length; ++i ) {
          markers[i].icon = icon.clone();
          var markerFeature = new OpenLayers.Feature(layer, new OpenLayers.LonLat(markers[i].lon, markers[i].lat), markers[i]);
          var marker = markerFeature.createMarker();
          
          //var marker = new OpenLayers.Marker(new OpenLayers.LonLat(markers[i].lon, markers[i].lat), icon.clone());
          marker.events.register('click', markerFeature, function(e) { self.markerClicked(e, this.data); } );
          marker.icon.imageDiv.appendChild(
            $('<div class="marker_title"><div class="left"/><div class="text"/><div class="right"/><div class="tail"/></div>')
              .css({position: 'absolute', top: 0, left: 11, 'white-space': 'nowrap', color: '#FFF', 'font-size':16, display: 'none', 'font-weight': 'bold', 'z-index': 400})
              .find('.text').html(markers[i].title).css({position: 'relative', 'z-index': 401, color: '#000'})
              .end()[0]);
          
          $(marker.icon.imageDiv).css('cursor', 'pointer').hover(function(e) { 
              if( self.isSmallMap ) {
                return;
              }
              var title = $(this).find('.marker_title').css({display: 'block'}); 
              var icon_offset = self.options.marker_icon_offset;
              
              title.css({top: -1 * title.height() - icon_offset.top});
              
              var tail = title.find('.tail');
              if( title.offset().left - self._map_container.offset().left + title.width() > self._map_container.width() || (title.offset().left > self._map_container.offset().left && parseInt(title.css('left'), 10) < -1*icon_offset.left - (tail.width()/2)) ) {
                title.css({left: -1 * title.width() - icon_offset.left + (tail.width()/2)});
                tail.css({left: title.width()-tail.width()});
              } else {
                title.css({left: -1*icon_offset.left - (tail.width()/2)});
                tail.css({left: 0});
              }
            }, function(e) { 
              $(this).find('.marker_title').css({display: 'none'}); 
            });
          
          layer.addMarker(marker);
          
        }
      }
    },
    
    /**
     * Handles when a marker is clicked.
     */
    markerClicked: function(e, data) {
      //make the map big if we are showing the small map
      if( this.isSmallMap ) {
        this.map_click_function(e);
        return;
      }
      
      var self = this;
      //render the details display
      try {
        if( this._detail_container ) {
          this._detail_container.remove();
        }
        var detail = new Display[this.options.display](this);
        this._detail_container = detail.render(data);
        this._container.prepend(this._detail_container);
        
        //hide all markers but the selected.
        var markers = this.getData().markers;
        for( var i = 0; i < markers.length; ++i ) {
          $(markers[i].icon.imageDiv).css({opacity: 0}).find('img').css({opacity: 0});
        }
        $(data.icon.imageDiv).css({opacity: 1}).find('img').css({opacity: 1}).css({filter: ''});
        
        //deactivate controls
        for( i = 0; i < this._map_controls.length; ++i ) {
          this._map_controls[i].deactivate();
        }
        
        this._map_container.animate({height: 200, width: 150}, {complete: function() { self._map_container.children().css({height: 200, width: 150}); self._map.updateSize(); self._map.setCenter(new OpenLayers.LonLat(data.lon, data.lat)); } }); //step: function(e) { console.log(e); if( Math.floor(e) % 80 ) { self._map.setCenter(new OpenLayers.LonLat(data.lon, data.lat)); } },
        $('.olControlNoSelect').css({opacity: 0});
        this.map_click_function = function(e) { self.smallMapClicked(e, data); };
        this._map.events.register('click', this._map, this.map_click_function);
        this.isSmallMap = true;
      } catch(ex) { log(ex); }
    },
    
    /**
     * Handles when the map is clicked on when it is small.
     */
    smallMapClicked: function(e, data) {
      var self = this;
      this._map.events.unregister('click', this._map, this.map_click_function);
      
      self._map_container.children().css({height: this.options.height, width: this.options.width}); 
      self._map.updateSize(); 
      self._map.setCenter(new OpenLayers.LonLat(data.lon, data.lat));
      
      //show all markers again.
      var markers = this.getData().markers;
      for( var i = 0; i < markers.length; ++i ) {
        $(markers[i].icon.imageDiv).css({opacity: 1}).css({filter: ''}).find('img').css({opacity: 1}).css({filter: ''});
      }
      
      //add map controls back
      for( i = 0; i < this._map_controls.length; ++i ) {
        this._map_controls[i].activate();
      }
      
      this._map_container.animate({height: this.options.height, width: this.options.width}); //, {step: function(e) { if( Math.floor(e) % 230 ) { self._map.setCenter(new OpenLayers.LonLat(data.lon, data.lat)); } }, complete: function() { self._map.updateSize(); self._map.setCenter(new OpenLayers.LonLat(data.lon, data.lat)); } });
      $('.olControlNoSelect').css({opacity: 1}).css({filter: ''});
      this.isSmallMap = false;
    }
    
  });
  
  /** @namespace Holds display type objects. */
  var Display = {};
  
  /** @lends Display.DetailDisplay# */
  Display.DetailDisplay = Class.extend({
    parent: null,
    
    /** 
     * Instantiates the detail display
     * @class Base class for all informational displays that can be placed in an InteractiveMap.
     * @constructs
     * @param {InteractiveMap} parent The interactive map parent.  (this is required because we need options from the parent)
     */
    init: function(parent) {
      this.parent = parent;
    },
    
    /**
     * Renders the detail display with the given data.
     * @param {object} data The data containing the details. (fields may be specific to display type)
     */
    render: function(data) {
      var out = '';
      
      for( var key in data ) {
        out += key +': '+ data[key] + '\n';
      }
      
      return $('<div></div>').html(out);
    }
  });
  
  /** @lends Display.GuamDisplay# */
  Display.GuamDisplay = Display.DetailDisplay.extend({
    str360: ['<object classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" width="800" height	="616" codebase="http://www.apple.com/qtactivex/qtplugin.cab">',
        '<param name = "src" value = "{SOURCE}"/>',
        '<param name = "autoplay" value = "true"/>',
        '<param name = "controller" value = "true"/>',
        '<param name = "scale" value = "tofit"/>',
        '<embed src = "{SOURCE}"	width="800"	height="600" cache="true" controller="true" scale="tofit"	pluginspage="http://www.apple.com/quicktime/download"></embed>',
        '</object>'].join('\n'),
    
    /** 
     * Instantiates the detail display
     * @class Interactive Guam map display.
     * @augments Display.DetailDisplay
     * @constructs
     * @param {InteractiveMap} parent The interactive map parent.  (this is required because we need options from the parent)
     */
    init: function(parent) {
      this._super(parent);
    },
    
    /**
     * Renders the detail display with the given data.
     * @param {object} data The data containing the details. (fields may be specific to display type)
     */
    render: function(data) {
      var self = this;
      var container = $('<div><div class="left"/><div class="right"/></div>');
      var left = container.find('.left');
      var right = container.find('.right');
      
      //required styles
      container.css({position: 'absolute', top: 0, left: 0, 'background-color': '#FFFFFF', width: this.parent.options.width, height: this.parent.options.height, overflow: 'hidden'});
      left.css({position: 'absolute', top: 0, left: 0, 'padding-top': 200, width: 150});
      right.css({position: 'absolute', top: 0, left: 170, width: this.parent.options.width - 170});
      
      //populate the left side
      var retToMap = $('<a>Return to Map >></a>').css({display: 'block', cursor: 'pointer', margin: '8px 0px'}).click(function() { if( self.parent ) { self.parent._map.events.triggerEvent('click', null); } });
      left.append(retToMap);
      
      var thm_360 = $('<img/>').attr({src: data.thumb_360, alt: data.title, title: 'Explore 360 degree view'}).css({cursor: 'pointer'});
      var thm_360_contain = $('<div style="position: relative;"><div style="position: absolute; top: 0; left: 0;">360&deg;</div></div>');
      thm_360_contain.find('div').css({opacity: 0.7, color: '#FFF', 'font-size': 50, 'text-align': 'center', 'margin-top': 25, width: 150});
      thm_360_contain.append(thm_360);
      left.append(thm_360_contain);
      thm_360_contain.click(function() { showModal(self.str360.replace(/\{SOURCE\}/g, data.url_360), true); });
      
      //populate the right side
      right.append($('<h2/>').html(data.title).css({height: 20, 'line-height': '22px', overflow: 'hidden'}));
      
      right.append($('<div/>').html(data.description).css({/* height: this.parent.options.height - 43 - 105, */ overflow: 'auto'}));
      
      if( data.thumbs ) {
        for( var i = 0; i < data.thumbs.length && i < 3; ++i ) {
          var img_holder = $('<div/>').css({"float": 'left', width: 110, height: 100, overflow: 'hidden', margin: '5px 2px', cursor: 'pointer'});
          img_holder.append($('<img/>').attr({src: data.thumbs[i], alt: data.title, title: 'View larger image'}));
          right.append(img_holder);
          
          //set up click handler
          img_holder.click((function(data, i) { return function() { showModal('<img src="'+ data.images[i] +'" alt="'+ data.title +'"/>'); }; })(data, i));
        }
      }
      
      //return the container so that the interactive map can place it.
      return container;
    }
    
  });
  
})(jQuery);

