/**
 * Copyright (c) 2005 Kai Hänninen <kai.hanninen@mbconcert.fi> 
 *
 * This file is part of PrimaGIS.
 *
 * PrimaGIS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PrimaGIS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PrimaGIS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * $Id: primagis.js.dtml 315 2005-11-02 15:27:30Z dokai $
 *
 * Notes about the PrimaGIS Javascript code for developers:
 *  - PrimaGIS uses the Mochikit library. See http://mochikit.org/
 *  - All global names (variable and function names) are prefixed with 'pg'.
 *  - Map operations are implemented using asynchronous calls to the server with
 *    callback methods.
 *  - Callback methods have a '__cb' postfix.
 *  - Event handlers are set for 'onmousedown', 'onmouseup' and 'onmousemove'
 *    events for the map image container element ('mapzone').
 *  - Some variables are initialized using DTML to access values from the PrimaGIS
 *    objects in Plone to avoid redundant data flow.
 */

/** URI to the PrimaGIS instance */
var pgURI = 'http://www.primagis.fi/demo/index_html';

/** Running sequence number to differentiate map views */
var pgMapSequence = (new Date()).getTime() % 1000000;

/** Map action identifiers */
var pgMapActions = { 'ZOOM'           : 'ZOOM',
                     'AREA_PIXEL'     : 'AREA_PIXEL',
                     'AREA_SPATIAL'   : 'AREA_SPATIAL',
                     'CENTER_PIXEL'   : 'CENTER_PIXEL',
                     'CENTER_SPATIAL' : 'CENTER_SPATIAL',
                     'SET_SIZE'       : 'SET_SIZE',
                     'SET_LAYERS'     : 'SET_LAYERS',
                     'REVERT_VIEW'    : 'REVERT_VIEW',
                     'RESET_VIEW'     : 'RESET_VIEW',
                     'ADD_FEATURE'    : 'ADD_FEATURE'};

/** Navigation modes */
var pgNavigationModes = { 'ZOOM_IN'  : 1,
                          'ZOOM_OUT' : 2,
                          'RECENTER' : 3,
                          'PAN'      : 4,
                          'ADD'      : 5 };
                          
/* Current navigation mode. If you change the default, remember to reflect the
 * change in the CSS class for the corresponding mode in map_templates.pt
 */
var pgMode = pgNavigationModes.PAN;

/* Flag for refreshing the legend. Some operations require that the map legend
 * be also refreshed, but only after the initial operation has finished. Setting
 * this flag to 'true', will schedule a legend refresh operation after the map
 * has been refreshed. The flag will be cleared automatically.
 */
var pgDoRefreshLegend = false;

/* Minimum selection threshold in pixels. Both dimensions of a rectangular
 * selection area must exceed this value for the operation to be recognized as a 
 * selection.
 */
var pgSelectionThreshold = 10;

/* The current map image size in pixels. These values are initialized with the
 * values from the PrimaGIS object and updated according to user session changes.
 */
var pgMapSize = { 'width' : 500,
                  'height' : 400 };

/** Flag that determines an ongoing dragging action */
var pgDragging = false;

/** Coordinates of an initial mouse press */
var pgMousePress = { 'x' : 0, 'y' : 0 };

/** Position and dimensions of the zoom box */
var pgZoomBox = {'left': 0, 'top': 0, 'width': 0, 'height': 0 };

/** Drap pan temporaries */
var pgDragTemp = { 'x1' : 0, 'y1' : 0, 'x2' : 0, 'y2' : 0 };

/** Absolute event coordinates reported by Opera browser */
var pgOperaAbsolute = {'x' : 0, 'y' : 0 };
/** Node relative coordinates reported by Opera browser */
var pgOperaRelative = { 'x' : -1, 'y' : -1 };


/** Map scalebar */
var pgScalebar;

setTimeout("pgInitializeScalebar()", 1000);

/**
 * Initializes the scalebar if one is available.
 * The code for the function is generated by a PrimaGISScalebar object.
 */
function pgInitializeScalebar() {
      
pgScalebar = new ScaleBar(35771325.6576);
pgScalebar.displaySystem = 'metric';
pgScalebar.minWidth = 200;
pgScalebar.maxWidth = 300;
pgScalebar.divisions = 2;
pgScalebar.subdivision = 2;
pgScalebar.showMinorMeasures = false;
pgScalebar.abbreviateLabel = false;
pgScalebar.singleLine = false;
pgScalebar.resolution = 72;
pgScalebar.place('pg-scalebar');

  }

/**
 * Wraps query string parameters with a URI to the PrimaGIS map instance and
 * a running sequence number.
 *
 * @param qstring query parameters
 */
function pgQuery(qstring) {
  pgMapSequence++;
  return pgURI + qstring + '&amp;' + pgMapSequence;
}

/**
 * Updates the currently visible layers to match the user's selection.
 *
 * @param nodes an array of checkbox input fields
 */
function pgUpdateLayers(nodes) {
  /* Switch off any sticky popups */
  nd();
  
  var keys = ['action'];
  var values = [pgMapActions.SET_LAYERS];
  var query = '/getImageMapData?';
  var deferred;
  
  for (i=0; i < nodes.length; i++) {
    if (nodes[i].checked) {
      keys.push('activelayers:list');
    } else {
      keys.push('inactivelayers:list'); 
    }
    values.push(nodes[i].value);
  }
    
  deferred = loadJSONDoc(pgQuery(query + queryString(keys,values)));
  deferred.addCallback(pgRefreshMap__cb);

  /* Request that the legend be refreshed after the map has been updated. */
  pgDoRefreshLegend = true;  
  
  return false;
}
 
/**
 * Requests a map action to be executed and schedules an asynchonous  
 * redraw for the map image.
 *
 * @param params dictionary holding the parameters for the action
 * @return None
 */
function pgMapAction(params) {
  /* Switch off any sticky popups */
  nd();

  var query='/getImageMapData?';
  var deferred;
  
  /* Save changes to map size in local variables also */
  if (params.action == pgMapActions.SET_SIZE)
    pgMapSize = { 'width' : params.width, 'height' : params.height };

  deferred = loadJSONDoc(pgQuery(query + queryString(params)));
  deferred.addCallback(pgRefreshMap__cb);
}

/**
 * Callback method for refreshing the map scalebar.
 *
 * @param scale the new map scale denominator.
 */
function pgRefreshScalebar__cb(scale) {
  pgScalebar.update(scale);
}

/**
 * Callback method for refreshing the map image and the corresponding image map.
 *
 * @param data array of objects containing the data for re-creating the map.
 *             See PrimaGIS.getImageMapData() method for details.
 */ 
function pgRefreshMap__cb(data) {
  /* mapzone is the <div> node containing both the imagemap and the map image */
  var mapzone = getElement("mapzone");
  /* Get the current image size */
  var mapimg = getElement("mapimg");
  var width = mapimg.width;
  var height = mapimg.height;
  var deferred;
     
  /* Check for a size change */
  if (pgMapSize.width > 0 && pgMapSize.height > 0 &&
      (pgMapSize.width != width || pgMapSize.height != height)) {
      width = pgMapSize.width;
      height = pgMapSize.height;
      updateNodeAttributes(mapzone, {'style': {'width' : width + 'px',
                                               'height': height + 'px'}});
  }
  mapimg = null;

  var imageMapId = 'imagemap' + pgMapSequence;
  /* Put in the new imagemap in the mapzone */
  if (data == null || data.length==0)
    replaceChildNodes(mapzone);
  else
    replaceChildNodes(mapzone, createDOM('map',
                                         { 'name' : imageMapId,
                                           'id' : imageMapId },
                                         map(partial(createDOM, 'area'), data)));
  /* Put in a new map image and the zoom box */
  appendChildNodes(mapzone, IMG({'usemap' : '#' + imageMapId,
                                 'name' : 'mapimg',
                                 'id' : 'mapimg',
                                 'alt' : 'Demo',
                                 'src' : 'view?id=' + pgMapSequence,
                                 'width' : width,
                                 'height' : height,
                                 'style': {'position' : 'relative'}
                                }),
                            DIV({'id' : 'pgZoomBox'}));
  if (pgDoRefreshLegend) pgRefreshLegend();
  
  deferred = loadJSONDoc(pgQuery('/getMapScale?'));
  deferred.addCallback(pgRefreshScalebar__cb);
}

/**
 * Callback method for re-rendering the map legend.
 *
 * @param data the legend data in dictionary form.
 */
function pgRefreshLegend__cb(data) {
  var container = getElement("legend-items");

  var createRuleItem = function(params) {
    return LI({'class' : 'rulename'},
              IMG({'src' : params.url, 'class' : 'legendicon'}),
              params.title);
  };
  
  if (data == null || data.length == 0)
    replaceChildNodes(container);
  else
    replaceChildNodes(container, UL(null, map(createRuleItem, data)));
}


/**
 * Schedules a re-rendering of the the map legend.
 */
function pgRefreshLegend() {
  /* Clear the flag */
  pgDoRefreshLegend = false;
  var deferred = loadJSONDoc(pgQuery('/getLegendData?'));
  deferred.addCallback(pgRefreshLegend__cb);
}


/**
 * Sets the default view for the map. The current map extent (bbox) will be
 * used as the new default view.
 */
function pgSetDefaultView() {
  var deferred = doSimpleXMLHttpRequest(pgQuery('/setDefaultView?'));
  deferred.addCallback(pgShowMessage__cb);
}

/**
 * Sets the default view for the current session. The current map extent (bbox)
 * will be used as the new session default view.
 */
function pgSetSessionDefaultView() {
  var deferred = doSimpleXMLHttpRequest(pgQuery('/setSessionDefaultView?'));
  deferred.addCallback(pgShowMessage__cb);
}

/**
 * Callback method for displaying a message to the user.
 *
 * @param req an XMLHttpRequest object containing a response from a server.
 */
function pgShowMessage__cb(req) {
  alert(req.responseText);
}



/**
 * Pans the map to given direction the
 * for a distance of half of corresponding map axis 
 * length. That is, for north and south pans for half of the
 * height of the map and for west and east pans for half of
 * the width of the map.
 *
 * @param direction the direction to pan (north, south, east west)
 */
function pgConstantPan(direction) {

  var map = getElement('mapimg');
  var x, y;

  if(direction == 'north') {
    x = Math.round(map.width / 2.0);
    y = Math.round(map.height / 4.0);
  } else if(direction == 'south') {
    x = Math.round(map.width / 2.0);
    y = Math.round(map.height * 3.0 / 4.0);
  } else if(direction == 'east') {
    x = Math.round(map.width * 3.0 / 4.0);
    y = Math.round(map.height / 2.0);
  } else if(direction == 'west') {
    x = Math.round(map.width / 4.0);
    y = Math.round(map.height / 2.0);
  } else if(direction == 'northwest') {
    x = Math.round(map.width / 4.0);
    y = Math.round(map.height / 4.0);
  } else if(direction == 'northeast') {
    x = Math.round(map.width * 3.0/ 4.0);
    y = Math.round(map.height / 4.0);
  } else if(direction == 'southwest') {
    x = Math.round(map.width / 4.0);
    y = Math.round(map.height * 3.0 / 4.0);
  } else if(direction == 'southeast') {
    x = Math.round(map.width * 3.0 / 4.0);
    y = Math.round(map.height * 3.0 / 4.0);
  } else {
    return;
  }

  /* Request a new map image */
  pgMapAction({'action' : pgMapActions.CENTER_PIXEL,
               'x' : x,
               'y' : y});
}

/**
 * Sets the navigation mode and updates the navigation button styles
 * accordingly.
 */
function pgSetNaviMode(mode) {
  pgMode = mode;

  /* reset all buttons */
  setElementClass("zoomin", "navButton");
  setElementClass("zoomout", "navButton");
  setElementClass("pan", "navButton");
  setElementClass("recenter", "navButton");

  /* switch on the button for the current mode */
  if (mode == pgNavigationModes.ZOOM_IN) {
    getElement("zoom_factor").value = 0.5;
    setElementClass("zoomin", "navSelectedButton");

  } else if (mode == pgNavigationModes.ZOOM_OUT) {
    getElement("zoom_factor").value = 2.0;
    setElementClass("zoomout", "navSelectedButton");

  } else if (mode == pgNavigationModes.PAN) {
    getElement("zoom_factor").value = 1;
    setElementClass("pan", "navSelectedButton");
  
  } else if (mode == pgNavigationModes.RECENTER) {
    getElement("zoom_factor").value = 1;
    setElementClass("recenter", "navSelectedButton");
  } else {
    throw "Unknown navigation mode: " + mode;
  }
}


/**
 * Utility method for accessing the event object in a cross browser manner.
 *
 * @param evt the event object created by a particular browser
 * @return event object or null
 */
function pgGetEvent(evt) {
    return (evt) ? evt : ((window.event) ? event : null);
}

/**
 * Debug helper function that prints all the arguments on the browser status
 * toolbar separated by a space character.
 */
function showStatus() {
  var msg = "";
  window.status = "";
  for (var a=0; a < arguments.length; a++) {
    msg += arguments[a] + " ";
  }
  window.status = msg;
}

/* utility function adapted from Recipe 9.3, JS & DHTML Cookbook */
function getPositionedEventCoords(evt) {
    var elem = (evt.target) ? evt.target : evt.srcElement;
    var coords = {'left':0, 'top':0};

    if(evt.layerX) {	/* Mozilla */
        /* note: this won't account for borders, so if the */
        /* element has borders, the coordinates may be offset */
        coords.left = evt.layerX;
        coords.top = evt.layerY;
        if (elem.id == "pgZoomBox") {
            coords.left += pgZoomBox.left;
            coords.top += pgZoomBox.top;
        }
    } else if (window.opera) {
        /* The Opera browser is problematic because evt.offsetX and evt.offsetY
         * report the relative coordinates correctly only about 9/10 times and
         * sometimes return random values. This results in the zoom box to
         * flicker and makes using it difficult.
         *
         * The problem is solved by using a combination of relative and absolute
         * coordinates reported by Opera. The coordinates of the point where the
         * zoom box area starts are read from the evt.offsetX and evt.offsetY
         * values, which are in most cases correct.
         * Then the size and dimensions of the zoom box are calculated by
         * differences in the absolute coordinates (using evt.clientX and
         * evt.clientY) which are also saved when the dragging was started and
         * compared to the current values.
         */
        if (evt.type == "mousedown" || evt.type == "mouseup") {
            coords.left = evt.offsetX;
            coords.top = evt.offsetY;
        } else {
            coords.left = pgOperaRelative.x + (evt.clientX - pgOperaAbsolute.x);
            coords.top = pgOperaRelative.y + (evt.clientY - pgOperaAbsolute.y);
        }
        /* Opera does not support alpha blending, so disable the feature */
        getElement("pgZoomBox").style.backgroundColor = "transparent";
    } else if (evt.x) {	/* IE, Konqueror */
        coords.left = evt.x;
        coords.top = evt.y;
    } else if (evt.offsetX) { /* fallback just in case */
        coords.left = evt.offsetX;
        coords.top = evt.offsetY;
        if (elem.id == "pgZoomBox") {
            coords.left += pgZoomBox.left;
            coords.top += pgZoomBox.top;
        }
    }
    
    if (elem.id != "pgZoomBox" && elem.id != "mapimg")
        return false;

    return coords;
}


/**
 * Event handler for the 'onmousedown' event.
 *
 * @param evt the corresponding event object
 */
function pgMouseDown(evt) {
  
    if (pgMode == pgNavigationModes.ZOOM_OUT || pgMode == pgNavigationModes.RECENTER) {
        return;
    }

    evt = pgGetEvent(evt);
    if(!evt) return false;

    var coords = getPositionedEventCoords(evt);
    var zoombox = getElement("pgZoomBox");

    if (pgMode == pgNavigationModes.PAN) {
        /* Hide zoom box if we are in pan mode */
        hideElement(zoombox);
        pgDragTemp.y1 = pgDragTemp.y2 = evt.clientY;
        pgDragTemp.x1 = pgDragTemp.x2 = evt.clientX;
    } else {
        pgZoomBox.top = pgMousePress.y = coords.top;
        pgZoomBox.left = pgMousePress.x = coords.left;
        pgZoomBox.width = 0;
        pgZoomBox.height = 0;
        
        /* The Opera browser (version 8.5) needs some special care, since the 
         * event object coordinates cannot be entirely trusted.
         */
        pgOperaRelative = { 'x' : evt.offsetX, 'y' : evt.offsetY };
        pgOperaAbsolute = { 'x' : evt.clientX, 'y' : evt.clientY };
        
        updateNodeAttributes(zoombox, { 'style' : { 'top' : coords.top + 'px',
                                                    'left' : coords.left + 'px',
                                                    'width' : 0,
                                                    'height' : 0 }});
        showElement(zoombox);
    }

    pgDragging = true;
    return false;
}

/**
 * Event handler for the 'onmousemove' event.
 * Currently two operations cause this event that we are interested in:
 * Zoom-in and drag panning.
 *
 * @param evt the event object
 */
function pgMouseMove(evt) {
  if (!pgDragging) return;

  evt = pgGetEvent(evt);
  if(!evt) return false;
  
  if (pgMode == pgNavigationModes.ZOOM_IN) {
    var coords = getPositionedEventCoords(evt);
    
    if (!coords) return false;
    
    var selection = {'top': 0, 'left': 0, 'height': 0, 'width': 0};

    /* calculate the position and dimensions of the selection */
    selection.height = Math.abs(coords.top - pgMousePress.y);
    selection.width = Math.abs(coords.left - pgMousePress.x);
    selection.left = (coords.left < pgMousePress.x) ? coords.left : pgMousePress.x;
    selection.top = (coords.top < pgMousePress.y) ? coords.top : pgMousePress.y;

    /* check for bad values: if outside area, do nothing */
    if(selection.top < 0 || selection.left < 0 
       || selection.left + selection.width >= pgMapSize.width
       || selection.top + selection.height >= pgMapSize.height)
        return false;
  
    /* update the zoom box */
    pgZoomBox = selection;
    updateNodeAttributes("pgZoomBox", { 'style' : { 'top' : selection.top + 'px',
                                                    'left' : selection.left + 'px',
                                                    'width' : selection.width + 'px',
                                                    'height' : selection.height + 'px' }});

  }  

  if (pgMode == pgNavigationModes.PAN) {
    /* Save the end coordinates of the drap operation */
    pgDragTemp.x2 = evt.clientX;
    pgDragTemp.y2 = evt.clientY;

    /* Update map image position */
    var newTop = (pgDragTemp.y2-pgDragTemp.y1);
    var newLeft = (pgDragTemp.x2-pgDragTemp.x1);
    updateNodeAttributes("mapimg", { 'style' : { 'top' : newTop + 'px',
                                                 'left' : newLeft + 'px' }});
  }    
  return false;
}

/**
 * The mouse-up event handler.
 * This method is responsible for deciding which action to execute based
 * on the user interaction.
 *
 * @param evt the mouse up event
 */
function pgMouseUp(evt) {
    pgDragging = false;
    evt = pgGetEvent(evt);
    if (!evt) return false;

    if (pgMode == pgNavigationModes.RECENTER) {
        var coords = getPositionedEventCoords(evt);

        pgMapAction({'action' : pgMapActions.CENTER_PIXEL,
                     'x' : coords.left,
                     'y' : coords.top});

    } else if (pgMode == pgNavigationModes.PAN) {
        var mi = getElement("mapimg");	    
        var dx = (pgDragTemp.x1 - pgDragTemp.x2);
        var dy = (pgDragTemp.y1 - pgDragTemp.y2);
        
        if (Math.abs(dx)<2 && Math.abs(dy)<2) {
          return false;
        } else {
          x = (mi.width / 2) + dx;
          y = (mi.height / 2) + dy;
        }
        pgMapAction({'action' : pgMapActions.CENTER_PIXEL,
                     'x' : Math.abs(x),
                     'y' : Math.abs(y)});

    } else if (pgMode == pgNavigationModes.ZOOM_IN) {
        if (pgZoomBox.width < pgSelectionThreshold || pgZoomBox.height < pgSelectionThreshold) {
            // If the selection is smaller than the threshold rectangle we ignore it.
            return false;
        }
        pgMapAction({'action' : pgMapActions.AREA_PIXEL,
                     'bbox' : pgZoomBox.left + ',' + 
                              (pgZoomBox.top + pgZoomBox.height) + ',' +
                              (pgZoomBox.left + pgZoomBox.width) + ',' +
                              pgZoomBox.top });

    } else if (pgMode == pgNavigationModes.ZOOM_OUT) {
        var zoomfactor = getElement("zoom_factor") ? getElement("zoom_factor").value : 1;
        var coords = getPositionedEventCoords(evt);

        pgMapAction({'action' : pgMapActions.ZOOM,
                     'factor' : zoomfactor});
        // TODO: shall we allow centering also, as before?

    } else if (pgMode == pgNavigationModes.ADD) {
        var layers = getElement("add_object");
        for (var i=1; i < layers.length; i++) {
            if (layers[i].selected) {
                var coords = getPositionedEventCoords(evt);

                /* Redirect to action() method */
                document.location.replace(pgURI + '/action?' +
                                          queryString({'action' : pgMapActions.ADD_FEATURE,
                                                       'x' : coords.left,
                                                       'y' : coords.top,
                                                       'layerid' : layers[i].value}));
            }
        }
    }
    return false;
}

/**
 * Sets the event handlers for the map.
 */ 
function pgSetEventHandlers() {
    var imgcont = getElement("mapzone");
    imgcont.onmousedown = pgMouseDown;
    imgcont.onmousemove = pgMouseMove;
    imgcont.onmouseup = pgMouseUp;
}


