/**
  * Outputs a map panel.
  * The map panel can be augmented by adding any of the following controls which automatically link themselves
  * to the map:
  * <ul>
  * <li>{@link sref_textbox()}</li>
  * <li>{@link sref_system_select()}</li>
  * <li>{@link sref_and_system()}</li>
  * <li>{@link georeference_lookup()}</li>
  * <li>{@link location_select()}</li>
  * <li>{@link location_autocomplete()}</li>
  * <li>{@link postcode_textbox()}</li>
  * </ul>
  * To run JavaScript at the end of map initialisation, add a function to the global array
  * called mapInitialisationHooks. Code cannot access the map at any previous point because
  * maps may not be initialised when the page loads, e.g. if the map initialisation is
  * delayed until the tab it is on is shown.
  * To run JavaScript which updates any of the map settings, add a function to the
  * mapSettingsHooks global array. For example this is used to configure the map by report
  * parameters panels which need certain tools on the map.
  * @param array $options Associative array of options to pass to the jQuery.indiciaMapPanel plugin.
  * Has the following possible options:
  * <ul><li><b>indiciaSvc</b><br/>
  * </li>
  * <li><b>indiciaGeoSvc</b><br/>
  * </li>
  * <li><b>readAuth</b><br/>
  * Provides read authentication tokens for the warehouse. Only required when there is a location control linked to the warehouse associated with this map.
  * </li>
  * <li><b>height</b><br/>
  * Height of the map panel, in pixels.
  * </li>
  * <li><b>width</b><br/>
  * Width of the map panel, in pixels or as a percentage if followed by a % symbol.
  * </li>
  * <li><b>initial_lat</b><br/>
  * Latitude of the centre of the initially displayed map, using WGS84.
  * </li>
  * <li><b>initial_long</b><br/>
  * Longitude of the centre of the initially displayed map, using WGS84.
  * </li>
  * <li><b>initial_zoom</b><br/>
  * </li>
  * <li><b>scroll_wheel_zoom</b><br/>
  * Does the scroll wheel zoom the map in and out when over the map? Defaults to true. When using the scroll wheel to look up and down a data entry form
  * it can be easy to inadvertantly scroll the map, so it may be desirable to disable this feature in some cases.
  * </li>
  * <li><b>proxy</b><br/>
  * </li>
  * <li><b>displayFormat</b><br/>
  * </li>
  * <li><b>presetLayers</b><br/>
  * Array of preset layers to include. Options are 'google_physical', 'google_streets', 'google_hybrid',
  * 'google_satellite', 'openlayers_wms', 'nasa_mosaic', 'virtual_earth' (deprecated, use bing_aerial),
  * 'bing_aerial', 'bing_hybrid, 'bing_shaded', 'osm' (for OpenStreetMap), 'osm_th' (for OpenStreetMap Tiles@Home).
  * </li>
  * <li><b>tilecacheLayers</b><br/>
  * Array of layer definitions for tilecaches, which are pre-cached background tiles. They are less flexible but much faster
  * than typical WMS services. The array is associative, with the following keys:
  *   caption - The display name of the layer
  *   servers - array list of server URLs for the cache
  *   layerName - the name of the layer within the cache
  *   settings - any other settings that need to be passed to the tilecache, e.g. the server resolutions or file format.</li>
  * <li><b>indiciaWMSLayers</b><br/>
  * </li>
  * <li><b>indiciaWFSLayers</b><br/>
  * </li>
  * <li><b>layers</b><br/>
  * An array of JavaScript variables which point to additional OpenLayers layer objects to add to the map. The JavaScript for creating these layers 
  * can be added to data_entry_helper::$onload_javascript before calling the map_panel method.
  * </li>
  * <li><b>clickableLayers</b><br/>
  * If support for clicking on a layer to provide info on the clicked objects is required, set this to an array containing the JavaScript variable
  * names for the OpenLayers WMS layer objects you have created for the clickable layers. The JavaScript for creating these layers 
  * can be added to data_entry_helper::$onload_javascript before calling the map_panel method and they can be the same layers as those referred to in 
  * the layers parameter.
  * </li>
  * <li><b>clickableLayersOutputMode</b><br/>
  * Set to popup to display the information retrieved from a click operation on a popup window, set to div to
  * display the information in a specified HTML div, or to customFunction to call a JavaScript function
  * after the click operation allowing custom functionality such as navigation to another page. Default 
  * is popup.
  * </li>
  * <li><b>clickableLayersOutputDiv</b><br/>
  * ID of the HTML div to output information retrieved from a click operation into, if clickableLayersOutputMode
  * is set to div.
  * </li>
  * <li><b>allowBox</b><br/>
  * Default true. Set to false to disable drag boxes for selecting items on clickable layers. The advantage of this is that
  * the drag boxes don't hinder attempts to drag the map to navigate.
  * </li>
  * <li><b>customClickFn</b>
  * Set to the name of a global custom JavaScript function which will handle the event of clicking on the map if 
  * you want custom functionality. Provide this when clickableLayersOutputMode=customFunction. The function will 
  * receive a single parameter containing an array of features.
  * </li>
  * <li><b>clickableLayersOutputFn</b><br/>
  * Allows overridding of the appearance of the output when clicking on the map for WMS or vector layers. Should be set to a 
  * JavaScript function name which takes a list of features and the map div as parameters, then returns the HTML to output.</li>
  * <li><b>clickableLayersOutputColumns</b><br/>
  * An associated array of column field names with column titles as the values which defines the columns that are output when clicking on a data point. 
  * If ommitted, then all columns are output using their original field names.
  * </li>
  * <li><b>locationLayerName</b><br/>
  * If using a location select or autocomplete control, then set this to the name of a feature type exposed on GeoServer which contains the id, name and boundary
  * geometry of each location that can be selected. Then when the user clicks on the map the system is able to automatically populate the locations control with the 
  * clicked on location. Ensure that the feature type is styled on GeoServer to appear as required, though it will be added to the map with semi-transparency. To use
  * this feature ensure that a proxy is set, e.g. by using the Indicia Proxy module in Drupal.
  * </li>
  * <li><b>locationLayerFilter</b><br/>
  * If using a location layer, then set this to a cql filter in order to select e.g. locations for a website or locations of a type.
  * The filter can act on any fields in the feature type that locationLayerName refers to.
  * </li>
  * <li><b>controls</b><br/>
  * </li>
  * <li><b>toolbarDiv</b><br/>
  * If set to 'map' then any required toolbuttons are output directly onto the map canvas (in the top right corner). Alternatively can be set to 'top',
  * 'bottom' or the id of a div on the page to output them into.
  * </li>
  * <li><b>toolbarPrefix</b><br/>
  * Content to include at the beginning of the map toolbar. Not applicable when the toolbar is added directly to the map.
  * </li>
  * <li><b>toolbarSuffix</b><br/>
  * Content to include at the end of the map toolbar. Not applicable when the toolbar is added directly to the map.
  * </li>
  * <li><b>helpDiv</b><br/>
  * Set to 'bottom' to add a div containing help hints below the map. Set to the name of a div to output help hints into that
  * div. Otherwise no help hints are displayed.
  * </li>
  * <li><b>clickForSpatialRef</b><br/>
  * Does clicking on the map set the spatial reference of the sample input controls on the form the map appears on (if any)?
  * Defaults to true.
  * </li>
  * <li><b>allowPolygonRecording</b><br/>
  * If a drawPolygon or drawLine control is present, do these set the spatial reference of the sample input controls on the form the map appears on (if any)?
  * The spatial ref is set to the polygon centroid and the sample geometry is set to the polygon itself allowing polygons for records.
  * </li>
  * <li><b>editLayer</b><br/>
  * </li>
  * <li><b>editLayerName</b><br/>
  * </li>
  * <li><b>standardControls</b>
  * An array of predefined controls that are added to the map. Select from:<br/>
  *    layerSwitcher - a button in the corner of the map which opens a panel allowing selection of the visible layers.<br/>
  *    drawPolygon - a tool for drawing polygons onto the map edit layer.<br/>
  *    drawLine - a tool for drawing lines onto the map edit layer.<br/>
  *    drawPoint - a tool for drawing points onto the map edit layer.<br/>
  *    zoomBox - allow zooming to a bounding box, drawn whilst holding the shift key down. This functionality is provided by the panZoom and panZoomBar controls as well
  *    so is only relevant when they are not selected. 
  *    panZoom - simple controls in the corner of the map for panning and zooming.
  *    panZoomBar - controls in the corner of the map for panning and zooming, including a slide bar for zooming.
  *    modifyFeature - a tool for selecting a feature on the map edit layer then editing the vertices of the feature.
  *    selectFeature - a tool for selecting a feature on the map edit layer.
  *    hoverFeatureHighlight - highlights the feature on the map edit layer which is under the mouse cursor position.
  *    fullscreen - add a button allowing the map to be shown in full screen mode.
  * Default is layerSwitcher, panZoom and graticule.
  * </li>
  * <li><b>initialFeatureWkt</b><br/>
  * Well known text for a geometry to load onto the map at startup, normally corresponding to the geometry of the record
  * being edited.
  * </li>
  * <li><b>initialBoundaryWkt</b><br/>
  * Well known text for a geometry to load onto the map at startup, normally corresponding to the geometry of the boundary
  * being edited (e.g. a site boundary).
  * </li>
  * <li><b>defaultSystem</b><br/>
  * </li>
  * <li><b>latLongFormat</b><br/>
  * Override the format for display of lat long references. Select from D (decimal degrees, the default), DM (degrees and decimal minutes)
  * or DMS (degrees, minutes and decimal seconds).</li>
  * <li><b>srefId</b><br/>
  * Override the id of the control that has the grid reference value
  * </li>
  * <li><b>srefSystemId</b><br/>
  * Override the id of the control that has the spatial reference system value
  * </li>
  * <li><b>geomId</b><br/>
  * </li>
  * <li><b>clickedSrefPrecisionMin</b><br/>
  * Specify the minimum precision allowed when clicking on the map to get a grid square. If not set then the grid square selected will increase to its maximum
  * size as the map is zoomed out. E.g. specify 4 for a 1km British National Grid square.
  * </li>
  * <li><b>clickedSrefPrecisionMax</b><br/>
  * Specify the maximum precision allowed when clicking on the map to get a grid square. If not set then the grid square selected will decrease to its minimum
  * size as the map is zoomed in. E.g. specify 4 for a 1km British National Grid square.
  * </li>
  * <li><b>msgGeorefSelectPlace</b><br/>
  * </li>
  * <li><b>msgGeorefNothingFound</b><br/>
  * </li>
  * <li><b>msgSrefOutsideGrid</b><br/>
  * Message displayed when point outside of grid reference range is clicked.
  * </li>
  * <li><b>msgSrefNotRecognised</b><br/>
  * Message displayed when a grid reference is typed that is not recognised.
  * </li>
  * <li><b>maxZoom</b><br/>
  * Limit the maximum zoom used when clicking on the map to set a point spatial reference. Use this to prevent over zooming on
  * background maps.</li>
  * <li><b>tabDiv</b><br/>
  * If loading this control onto a set of tabs, specify the tab control's div ID here. This allows the control to
  * automatically generate code which only generates the map when the tab is shown.</li>
  * <li><b>setupJs</b><br/>
  * When there is JavaScript to run before the map is initialised, put the JavaScript into this option. This allows the map to run the 
  * setup JavaScript just in time, immediately before the map is created. This avoids problems where the setup JavaScript causes the OpenLayers library 
  * to be initialised too earlier if the map is on a div.</li>
  * <li><b>graticuleProjection</b><br/>
  * EPSG code (including EPSG:) for the projection used for the graticule (grid overlay).</li>
  * <li><b>graticuleBounds</b><br/>
  * Array of the bounding box coordinates for the graticule(W,S,E,N) in the coordinate system of the graticule.</li>
  * <li><b>graticuleIntervals</b><br/>
  * A list of possible graticule widths in the coordinate system of the graticule.</li>
  * <li><b>graticuleIntervalColours</b><br/>
  * A list of possible graticule CSS colours corresponding to each graticule width.</li>
  * <li><b>rememberPos</b><br/>
  * Set to true to enable restoring the map position when the page is reloaded. Requires jquery.cookie plugin. As this feature
  * requires cookies, you should notify your users in compliance with European cookie law if you use this option.</li>
  * </ul>
  * <li><b>helpDiv</b><br/>
  * Set to bottom to output a help div under the map, or set to the ID of a div to output into.</li>
  * <li><b>helpToPickPrecisionMin</b><br/>
  * Set to a precision in metres (e.g. 10, 100, 1000) to provide help guiding the recorder to pick a grid square of at least that precision. Ensure that helpDiv is 
  * set when using this option.</li>
  * <li><b>helpToPickPrecisionMax</b><br/>
  * Set to a precision in metres (e.g. 10, 100, 1000) that the help system will accept as not requiring further refinement when a grid square of this precision is picked.</li>  
  * <li><b>helpToPickPrecisionSwitchAt</b><br/>
  * Set to a precision in metres (e.g. 10, 100, 1000) that the map will switch to the satellite layer (if Google or Bing satellite layers active) when
  * the recorder picks a grid square of at least that precision.</li>
  * <li><b>gridRefHint</b><br/>
  * Set to true to put the currently hovered over grid ref in an element with id grid-ref-hint. Use the next setting to automate adding this to the page.</li>
  * <li><b>gridRefHintInFooter</b><br/>
  * Defaults to true. If there is a grid ref hint, should it go in the footer area of the map? If so, there is no need to add an element id grid-ref-hint to the page.</li>
  * </ul>
  * @param array $olOptions Optional array of settings for the OpenLayers map object. If overriding the projection or
  * displayProjection settings, just pass the EPSG number, e.g. 27700.
  */
 public static function map_panel($options, $olOptions = array())
 {
     if (!$options) {
         return '<div class="error">Form error. No options supplied to the map_panel method.</div>';
     } else {
         global $indicia_templates;
         $presetLayers = array();
         // If the caller has not specified the background layers, then default to the ones we have an API key for
         if (!array_key_exists('presetLayers', $options)) {
             $presetLayers[] = 'google_satellite';
             $presetLayers[] = 'google_hybrid';
             $presetLayers[] = 'google_physical';
             $presetLayers[] = 'virtual_earth';
             $presetLayers[] = 'osm';
         }
         if (!empty(parent::$warehouse_proxy)) {
             $warehouseUrl = parent::$warehouse_proxy;
         } else {
             $warehouseUrl = parent::$base_url;
         }
         $options = array_merge(array('indiciaSvc' => $warehouseUrl, 'indiciaGeoSvc' => self::$geoserver_url, 'divId' => 'map', 'class' => '', 'width' => 600, 'height' => 470, 'presetLayers' => $presetLayers, 'jsPath' => self::$js_path, 'clickForSpatialRef' => true, 'gridRefHintInFooter' => true, 'gridRefHint' => false), $options);
         // When using tilecache layers, the open layers defaults cannot be used. The caller must take control of openlayers settings
         if (isset($options['tilecacheLayers'])) {
             $options['useOlDefaults'] = false;
         }
         //width and height may be numeric, which is interpreted as pixels, or a css string, e.g. '50%'
         //width in % is causing problems with panning in Firefox currently. 13/3/2010.
         if (is_numeric($options['height'])) {
             $options['height'] .= 'px';
         }
         if (is_numeric($options['width'])) {
             $options['width'] .= 'px';
         }
         if (array_key_exists('readAuth', $options)) {
             // Convert the readAuth into a query string so it can pass straight to the JS class.
             $options['readAuth'] = '&' . self::array_to_query_string($options['readAuth']);
             str_replace('&', '&amp;', $options['readAuth']);
         }
         // convert textual true/false to boolean equivalents.
         foreach ($options as $key => $value) {
             if ($options[$key] === "false") {
                 $options[$key] = false;
             } else {
                 if ($options[$key] === "true") {
                     $options[$key] = true;
                 }
             }
         }
         // Autogenerate the links to the various mapping libraries as required
         if (array_key_exists('presetLayers', $options)) {
             foreach ($options['presetLayers'] as $layer) {
                 $a = explode('_', $layer);
                 $a = strtolower($a[0]);
                 switch ($a) {
                     case 'google':
                         self::add_resource('googlemaps');
                         break;
                     case 'virtual':
                         self::add_resource('virtualearth');
                         break;
                 }
                 if ($a == 'bing' && (!isset(self::$bing_api_key) || empty(self::$bing_api_key))) {
                     return '<p class="error">To use the Bing layers, please ensure that you declare a variable called $bing_api_key in the helper_config.php file set to an ' . 'empty string and specify a Bing API Key on the IForm settings page.</p>';
                 }
             }
         }
         // This resource has a dependency on the googlemaps resource so has to be added afterwards.
         self::add_resource('indiciaMapPanel');
         if (array_key_exists('standardControls', $options)) {
             if (in_array('graticule', $options['standardControls'])) {
                 self::add_resource('graticule');
             }
             if (in_array('clearEditLayer', $options['standardControls'])) {
                 self::add_resource('clearLayer');
             }
         }
         // We need to fudge the JSON passed to the JavaScript class so it passes any actual layers, functions
         // and controls, not the string class names.
         $json_insert = '';
         $js_entities = array('controls', 'layers', 'clickableLayers');
         foreach ($js_entities as $entity) {
             if (array_key_exists($entity, $options)) {
                 $json_insert .= ',"' . $entity . '":[' . implode(',', $options[$entity]) . ']';
                 unset($options[$entity]);
             }
         }
         // Same for 'clickableLayersOutputFn'
         if (isset($options['clickableLayersOutputFn'])) {
             $json_insert .= ',"clickableLayersOutputFn":' . $options['clickableLayersOutputFn'];
             unset($options['clickableLayersOutputFn']);
         }
         // Same for 'customClickFn'
         if (isset($options['customClickFn'])) {
             $json_insert .= ',"customClickFn":' . $options['customClickFn'];
             unset($options['customClickFn']);
         }
         // make a copy of the options to pass into JavaScript, with a few entries removed.
         $jsoptions = array_merge($options);
         unset($jsoptions['setupJs']);
         unset($jsoptions['tabDiv']);
         if (isset(self::$bing_api_key)) {
             $jsoptions['bing_api_key'] = self::$bing_api_key;
         }
         $json = substr(json_encode($jsoptions), 0, -1) . $json_insert . '}';
         $olOptions = array_merge(array('theme' => self::$js_path . 'theme/default/style.css'), $olOptions);
         $json .= ',' . json_encode($olOptions);
         $javascript = '';
         $mapSetupJs = '';
         if (isset($options['setupJs'])) {
             $mapSetupJs .= $options['setupJs'] . "\n";
         }
         $mapSetupJs .= "jQuery('#" . $options['divId'] . "').indiciaMapPanel({$json});\n";
         // trigger a change event on the sref if it's set in case locking in use. This will draw the polygon on the map.
         $srefId = empty($options['srefId']) ? '$.fn.indiciaMapPanel.defaults.srefId' : "'{$options['srefId']}'";
         if (!(isset($options['switchOffSrefRetrigger']) && $options['switchOffSrefRetrigger'] == true)) {
             $mapSetupJs .= "      var srefId = {$srefId};\n" . "      if (srefId && \$('#' + srefId).length && \$('#' + srefId).val()!=='' && indiciaData.mapdiv.settings.initialBoundaryWkt===null) {jQuery('#'+srefId).change();}\n";
         }
         // If the map is displayed on a tab, so we must only generate it when the tab is displayed as creating the
         // map on a hidden div can cause problems. Also, the map must not be created until onload or later. So
         // we have to set use the mapTabLoaded and windowLoaded to track when these events are fired, and only
         // load the map when BOTH the events have fired.
         if (isset($options['tabDiv'])) {
             $divId = preg_replace('/[^a-zA-Z0-9]/', '', $options['divId']);
             $javascript .= "var mapTabHandler = function(event, ui) { \n";
             $javascript .= "  panel = typeof ui.newPanel==='undefined' ? ui.panel : ui.newPanel[0];\n";
             $javascript .= "  if (typeof indiciaData.mapdiv !== 'undefined' && \$(indiciaData.mapdiv).parents('#'+panel.id).length) {\n";
             $javascript .= "    indiciaData.mapdiv.map.updateSize();\n";
             $javascript .= "  }\n\n};\n";
             $javascript .= "indiciaFns.bindTabsActivate(\$(\$('#" . $options['tabDiv'] . "').parent()), mapTabHandler);\n";
             // Insert this script at the beginning, because it must be done before the tabs are initialised or the
             // first tab cannot fire the event
             self::$javascript = $javascript . self::$javascript;
         }
         $options['suffixTemplate'] = 'blank';
         self::$onload_javascript .= $mapSetupJs;
         $r = str_replace('{content}', self::apply_template('map_panel', $options), $indicia_templates['jsWrap']);
         if ($options['gridRefHintInFooter'] && $options['gridRefHint']) {
             $div = '<div id="map-footer" class="grid-ref-hints ui-helper-clearfix" style="width: ' . $options['width'] . '" ' . 'title="When you hover the mouse over the map, the grid reference is displayed here. Hold the minus key or plus key when clicking on the map ' . 'to decrease or increase the grid square precision respectively.">';
             if ($options['clickForSpatialRef']) {
                 $r .= $div . '<h3>' . lang::get('Click to set map ref') . '</h3>' . '<div class="grid-ref-hint hint-minus">' . '<span class="label"></span><span class="data"></span> <span>(' . lang::get('hold -') . ')</span></div>' . '<div class="grid-ref-hint hint-normal"><span class="label"> </span><span class="data"></span></div>' . '<div class="grid-ref-hint hint-plus">' . '<span class="label"></span><span class="data"></span> <span>(' . lang::get('hold +') . ')</span></div>';
             } else {
                 $r .= $div . '<h3>' . lang::get('Map ref at pointer') . '</h3>' . '<div class="grid-ref-hint hint-normal"><span class="label"></span><span class="data"></span></div>';
             }
             $r .= '</div>';
         }
         return $r;
     }
 }
Exemple #2
0
 /**
  * Outputs a map panel.
  * The map panel can be augmented by adding any of the following controls which automatically link themselves
  * to the map:
  * <ul>
  * <li>{@link sref_textbox()}</li>
  * <li>{@link sref_system_select()}</li>
  * <li>{@link sref_and_system()}</li>
  * <li>{@link georeference_lookup()}</li>
  * <li>{@link location_select()}</li>
  * <li>{@link location_autocomplete()}</li>
  * <li>{@link postcode_textbox()}</li>
  * </ul>
  * To run JavaScript at the end of map initialisation, add a function to the global array
  * called mapInitialisationHooks. Code cannot access the map at any previous point because
  * maps may not be initialised when the page loads, e.g. if the map initialisation is
  * delayed until the tab it is on is shown.
  * To run JavaScript which updates any of the map settings, add a function to the
  * mapSettingsHooks global array. For example this is used to configure the map by report
  * parameters panels which need certain tools on the map.
  * @param array $options Associative array of options to pass to the jQuery.indiciaMapPanel plugin.
  * Has the following possible options:
  * <ul><li><b>indiciaSvc</b><br/>
  * </li>
  * <li><b>indiciaGeoSvc</b><br/>
  * </li>
  * <li><b>readAuth</b><br/>
  * </li>
  * <li><b>height</b><br/>
  * </li>
  * <li><b>width</b><br/>
  * </li>
  * <li><b>initial_lat</b><br/>
  * Latitude of the centre of the initially displayed map, using WGS84.
  * </li>
  * <li><b>initial_long</b><br/>
  * Longitude of the centre of the initially displayed map, using WGS84.
  * </li>
  * <li><b>initial_zoom</b><br/>
  * </li>
  * <li><b>scroll_wheel_zoom</b><br/>
  * </li>
  * <li><b>proxy</b><br/>
  * </li>
  * <li><b>displayFormat</b><br/>
  * </li>
  * <li><b>presetLayers</b><br/>
  * </li>
  * <li><b>tilecacheLayers</b><br/>
  * Array of layer definitions for tilecaches, which are pre-cached background tiles. They are less flexible but much faster
  * than typical WMS services. The array is associative, with the following keys:
  *   caption - The display name of the layer
  *   servers - array list of server URLs for the cache
  *   layerName - the name of the layer within the cache
  *   settings - any other settings that need to be passed to the tilecache, e.g. the server resolutions or file format.</li>
  * <li><b>indiciaWMSLayers</b><br/>
  * </li>
  * <li><b>indiciaWFSLayers</b><br/>
  * </li>
  * <li><b>layers</b><br/>
  * An array of JavaScript variables which point to additional OpenLayers layer objects to add to the map. The JavaScript for creating these layers 
  * can be added to data_entry_helper::$onload_javascript before calling the map_panel method.
  * </li>
  * <li><b>clickableLayers</b><br/>
  * If support for clicking on a layer to provide info on the clicked objects is required, set this to an array containing the JavaScript variable
  * names for the OpenLayers WMS layer objects you have created for the clickable layers. The JavaScript for creating these layers 
  * can be added to data_entry_helper::$onload_javascript before calling the map_panel method and they can be the same layers as those referred to in 
  * the layers parameter.
  * </li>
  * <li><b>clickableLayersOutputDiv</b><br/>
  * If this is set to the name of a div, then clicking on a clickable layer item outputs the details into this div rather than a popup.
  * </li>
  * <li><b>clickableLayersOutputColumns</b><br/>
  * An associated array of column field names with column titles as the values which defines the columns that are output when clicking on a data point. 
  * If ommitted, then all columns are output using their original field names.
  * </li>
  * <li><b>clickableLayersOutputFn</b><br/>
  * Allows overridding of the appearance of the output when clicking on the map for WMS or vector layers. Should be set to a JavaScript function name 
  * which takes a list of features and the map div as parameters, then returns the HTML to output.</li>
  * <li><b>locationLayerName</b><br/>
  * If using a location select or autocomplete control, then set this to the name of a feature type exposed on GeoServer which contains the id, name and boundary
  * geometry of each location that can be selected. Then when the user clicks on the map the system is able to automatically populate the locations control with the 
  * clicked on location. Ensure that the feature type is styled on GeoServer to appear as required, though it will be added to the map with semi-transparency. To use
  * this feature ensure that a proxy is set, e.g. by using the Indicia Proxy module in Drupal.
  * </li>
  * <li><b>controls</b><br/>
  * </li>
  * <li><b>toolbarDiv</b><br/>
  * If set to 'map' then any required toolbuttons are output directly onto the map canvas (in the top right corner). Alternatively can be set to 'top',
  * 'bottom' or the id of a div on the page to output them into.
  * </li>
  * <li><b>toolbarPrefix</b><br/>
  * Content to include at the beginning of the map toolbar. Not applicable when the toolbar is added directly to the map.
  * </li>
  * <li><b>toolbarSuffix</b><br/>
  * Content to include at the end of the map toolbar. Not applicable when the toolbar is added directly to the map.
  * </li>
  * <li><b>editLayer</b><br/>
  * </li>
  * <li><b>editLayerName</b><br/>
  * </li>
  * <li><b>standardControls</b>
  * An array of predefined controls that are added to the map. Select from:<br/>
  *    layerSwitcher - a button in the corner of the map which opens a panel allowing selection of the visible layers.<br/>
  *    drawPolygon - a tool for drawing polygons onto the map edit layer.<br/>
  *    drawLine - a tool for drawing lines onto the map edit layer.<br/>
  *    drawPoint - a tool for drawing points onto the map edit layer.<br/>
  *    zoomBox - allow zooming to a bounding box, drawn whilst holding the shift key down. This functionality is provided by the panZoom and panZoomBar controls as well
  *    so is only relevant when they are not selected. 
  *    panZoom - simple controls in the corner of the map for panning and zooming.
  *    panZoomBar - controls in the corner of the map for panning and zooming, including a slide bar for zooming.
  *    modifyFeature - a tool for selecting a feature on the map edit layer then editing the vertices of the feature.
  *    selectFeature - a tool for selecting a feature on the map edit layer.
  *    hoverFeatureHighlight - highlights the feature on the map edit layer which is under the mouse cursor position.
  * Default is layerSwitcher, panZoom and graticule.
  * </li>
  * <li><b>initialFeatureWkt</b><br/>
  * </li>
  * <li><b>defaultSystem</b><br/>
  * </li>
  * <li><b>latLongFormat</b><br/>
  * Override the format for display of lat long references. Select from D (decimal degrees, the default), DM (degrees and decimal minutes)
  * or DMS (degrees, minutes and decimal seconds).</li>
  * <li><b>srefId</b><br/>
  * Override the id of the control that has the grid reference value
  * </li>
  * <li><b>srefSystemId</b><br/>
  * Override the id of the control that has the spatial reference system value
  * </li>
  * <li><b>geomId</b><br/>
  * </li>
  * <li><b>clickedSrefPrecisionMin</b><br/>
  * Specify the minimum precision allowed when clicking on the map to get a grid square. If not set then the grid square selected will increase to its maximum
  * size as the map is zoomed out. E.g. specify 4 for a 1km British National Grid square.
  * </li>
  * <li><b>clickedSrefPrecisionMax</b><br/>
  * Specify the maximum precision allowed when clicking on the map to get a grid square. If not set then the grid square selected will decrease to its minimum
  * size as the map is zoomed in. E.g. specify 4 for a 1km British National Grid square.
  * </li>
  * <li><b>msgGeorefSelectPlace</b><br/>
  * </li>
  * <li><b>msgGeorefNothingFound</b><br/>
  * </li>
  * <li><b>msgSrefOutsideGrid</b><br/>
  * Message displayed when point outside of grid reference range is clicked.
  * </li>
  * <li><b>msgSrefNotRecognised</b><br/>
  * Message displayed when a grid reference is typed that is not recognised.
  * </li>
  * <li><b>maxZoom</b><br/>
  * Limit the maximum zoom used when clicking on the map to set a point spatial reference. Use this to prevent over zooming on
  * background maps.</li>
  * <li><b>tabDiv</b><br/>
  * If loading this control onto a set of tabs, specify the tab control's div ID here. This allows the control to
  * automatically generate code which only generates the map when the tab is shown.</li>
  * <li><b>setupJs</b><br/>
  * When there is JavaScript to run before the map is initialised, put the JavaScript into this option. This allows the map to run the 
  * setup JavaScript just in time, immediately before the map is created. This avoids problems where the setup JavaScript causes the OpenLayers library 
  * to be initialised too earlier if the map is on a div.</li>
  * <li><b>setupJs</b><br/>
  * When there is JavaScript to run before the map is initialised, put the JavaScript into this option. This allows the map to run the 
  * setup JavaScript just in time, immediately before the map is created. This avoids problems where the setup JavaScript causes the OpenLayers library 
  * to be initialised too earlier if the map is on a div.</li>
  * <li><b>graticuleProjection</b><br/>
  * EPSG code (including EPSG:) for the projection used for the graticule (grid overlay).</li>
  * <li><b>graticuleBounds</b><br/>
  * Array of the bounding box coordinates for the graticule(W,S,E,N).</li>
  * <li><b>rememberPos</b><br/>
  * Set to true to enable restoring the map position when the page is reloaded. Requires jquery.cookie plugin.</li>
  * </ul>
  * @param array $olOptions Optional array of settings for the OpenLayers map object. If overriding the projection or
  * displayProjection settings, just pass the EPSG number, e.g. 27700.
  */
 public static function map_panel($options, $olOptions = null)
 {
     if (!$options) {
         return '<div class="error">Form error. No options supplied to the map_panel method.</div>';
     } else {
         global $indicia_templates;
         $presetLayers = array();
         // If the caller has not specified the background layers, then default to the ones we have an API key for
         if (!array_key_exists('presetLayers', $options)) {
             if (parent::$multimap_api_key != '') {
                 $defaultLayers[] = 'multimap_landranger';
             }
             $presetLayers[] = 'google_satellite';
             $presetLayers[] = 'google_hybrid';
             $presetLayers[] = 'google_physical';
             // Fallback as we don't need a key for this.
             $presetLayers[] = 'virtual_earth';
         }
         $options = array_merge(array('indiciaSvc' => self::$base_url, 'indiciaGeoSvc' => self::$geoserver_url, 'divId' => 'map', 'class' => '', 'width' => 600, 'height' => 470, 'presetLayers' => $presetLayers, 'jsPath' => self::$js_path), $options);
         // When using tilecache layers, the open layers defaults cannot be used. The caller must take control of openlayers settings
         if (isset($options['tilecacheLayers'])) {
             $options['useOlDefaults'] = false;
         }
         //width and height may be numeric, which is interpreted as pixels, or a css string, e.g. '50%'
         //width in % is causing problems with panning in Firefox currently. 13/3/2010.
         if (is_numeric($options['height'])) {
             $options['height'] .= 'px';
         }
         if (is_numeric($options['width'])) {
             $options['width'] .= 'px';
         }
         if (array_key_exists('readAuth', $options)) {
             // Convert the readAuth into a query string so it can pass straight to the JS class.
             $options['readAuth'] = '&' . self::array_to_query_string($options['readAuth']);
             str_replace('&', '&amp;', $options['readAuth']);
         }
         // Autogenerate the links to the various mapping libraries as required
         if (array_key_exists('presetLayers', $options)) {
             foreach ($options['presetLayers'] as $layer) {
                 $a = explode('_', $layer);
                 $a = strtolower($a[0]);
                 switch ($a) {
                     case 'google':
                         self::add_resource('googlemaps');
                         break;
                     case 'multimap':
                         self::add_resource('multimap');
                         break;
                     case 'virtual':
                     case 'bing':
                         self::add_resource('virtualearth');
                         break;
                 }
             }
         }
         // This resource has a dependency on the googlemaps resource so has to be added afterwards.
         self::add_resource('indiciaMapPanel');
         if (array_key_exists('standardControls', $options)) {
             if (in_array('graticule', $options['standardControls'])) {
                 self::add_resource('graticule');
             }
             if (in_array('clearEditLayer', $options['standardControls'])) {
                 self::add_resource('clearLayer');
             }
         }
         // We need to fudge the JSON passed to the JavaScript class so it passes any actual layers, functions
         // and controls, not the string class names.
         $json_insert = '';
         $js_entities = array('controls', 'layers', 'clickableLayers');
         foreach ($js_entities as $entity) {
             if (array_key_exists($entity, $options)) {
                 $json_insert .= ',"' . $entity . '":[' . implode(',', $options[$entity]) . ']';
                 unset($options[$entity]);
             }
         }
         // Same for 'clickableLayersOutputFn'
         if (isset($options['clickableLayersOutputFn'])) {
             $json_insert .= ',"clickableLayersOutputFn":' . $options['clickableLayersOutputFn'];
             unset($options['clickableLayersOutputFn']);
         }
         // make a copy of the options to pass into JavaScript, with a few entries removed.
         $jsoptions = array_merge($options);
         unset($jsoptions['setupJs']);
         unset($jsoptions['tabDiv']);
         if (isset(self::$bing_api_key)) {
             $jsoptions['bing_api_key'] = self::$bing_api_key;
         }
         $json = substr(json_encode($jsoptions), 0, -1) . $json_insert . '}';
         if ($olOptions) {
             $json .= ',' . json_encode($olOptions);
         }
         $javascript = '';
         $mapSetupJs = '';
         if (isset($options['setupJs'])) {
             $mapSetupJs .= $options['setupJs'] . "\n";
         }
         $mapSetupJs .= "jQuery('#" . $options['divId'] . "').indiciaMapPanel({$json});\n";
         // If the map is displayed on a tab, so we must only generate it when the tab is displayed as creating the
         // map on a hidden div can cause problems. Also, the map must not be created until onload or later. So
         // we have to set use the mapTabLoaded and windowLoaded to track when these events are fired, and only
         // load the map when BOTH the events have fired.
         if (isset($options['tabDiv'])) {
             $javascript .= "var tabHandler = function(event, ui) { \n";
             $javascript .= "  if (ui.panel.id=='" . $options['tabDiv'] . "') {\n";
             $javascript .= "    indiciaData.mapTabLoaded=true;\n";
             $javascript .= "    if (indiciaData.windowLoaded) {\n      ";
             $javascript .= $mapSetupJs;
             $javascript .= "    }\n    \$(this).unbind(event);\n";
             $javascript .= "  }\n\n};\n";
             $javascript .= "jQuery(jQuery('#" . $options['tabDiv'] . "').parent()).bind('tabsshow', tabHandler);\n";
             // Insert this script at the beginning, because it must be done before the tabs are initialised or the
             // first tab cannot fire the event
             self::$javascript = $javascript . self::$javascript;
             self::$onload_javascript .= "if (typeof indiciaData.mapTabLoaded!==\"undefined\") {\n{$mapSetupJs}\n}\n";
         } else {
             self::$onload_javascript .= $mapSetupJs;
         }
         return self::apply_template('map_panel', $options);
     }
 }