au.org.ala.biocache.web.WMSController.java Source code

Java tutorial

Introduction

Here is the source code for au.org.ala.biocache.web.WMSController.java

Source

/**************************************************************************
 *  Copyright (C) 2013 Atlas of Living Australia
 *  All Rights Reserved.
 *
 *  The contents of this file are subject to the Mozilla Public
 *  License Version 1.1 (the "License"); you may not use this file
 *  except in compliance with the License. You may obtain a copy of
 *  the License at http://www.mozilla.org/MPL/
 *
 *  Software distributed under the License is distributed on an "AS
 *  IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 *  implied. See the License for the specific language governing
 *  rights and limitations under the License.
 ***************************************************************************/
package au.org.ala.biocache.web;

import au.org.ala.biocache.dao.QidCacheDAO;
import au.org.ala.biocache.dao.SearchDAO;
import au.org.ala.biocache.dao.TaxonDAO;
import au.org.ala.biocache.dto.*;
import au.org.ala.biocache.model.Qid;
import au.org.ala.biocache.util.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections.map.LRUMap;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import javax.imageio.ImageIO;
import javax.inject.Inject;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
import java.util.List;

/**
 * This controller provides mapping services which include WMS services. Includes support for:
 *
 * <ul>
 *    <li>GetCapabilities</li>
 *    <li>GetMap</li>
 *    <li>GetFeatureInfo</li>
 *    <li>GetMetadata</li>
 * </ul>
 */
@Controller
public class WMSController extends AbstractSecureController {

    /**
     * webportal results limit
     */
    private final int DEFAULT_PAGE_SIZE = 1000000;

    @Value("${wms.colour:0x00000000}")
    private int DEFAULT_COLOUR;
    /**
     * webportal image max pixel count
     */
    @Value("${wms.image.pixel.count:36000000}")
    private int MAX_IMAGE_PIXEL_COUNT; //this is slightly larger than 600dpi A4
    /**
     * legend limits
     */
    private final String NULL_NAME = "Unknown";
    /**
     * max uncertainty mappable in m
     */
    @Value("${wms.uncertainty.max:30000}")
    private double MAX_UNCERTAINTY;
    /**
     * add pixel radius for wms highlight circles
     */
    @Value("${wms.highlight.radius:3}")
    private static int HIGHLIGHT_RADIUS;
    /**
     * Global wms cache enable. WMS requests can disable adding to the cache using CACHE=off. WMS CACHE=off will still
     * read from the cache.
     */
    @Value("${wms.cache.enabled:true}")
    private boolean wmsCacheEnabled;
    @Value("${qid.wkt.maxPoints:5000}")
    private int maxWktPoints;
    /**
     * Logger initialisation
     */
    private final static Logger logger = Logger.getLogger(WMSController.class);
    /**
     * Fulltext search DAO
     */
    @Inject
    protected SearchDAO searchDAO;
    @Inject
    protected QueryFormatUtils queryFormatUtils;
    @Inject
    protected TaxonDAO taxonDAO;
    @Inject
    protected SearchUtils searchUtils;
    @Inject
    protected QidCacheDAO qidCacheDAO;
    @Inject
    protected WMSCache wmsCache;
    /**
     * Load a smaller 256x256 png than java.image produces
     */
    final static byte[] blankImageBytes;

    @Value("${webservices.root:http://biocache.ala.org.au/ws}")
    protected String baseWsUrl;
    @Value("${geoserver.url:http://spatial.ala.org.au/geoserver}")
    protected String geoserverUrl;

    @Value("${organizationName:Atlas of Living Australia}")
    protected String organizationName;
    @Value("${orgCity:Canberra}")
    protected String orgCity;
    @Value("${orgStateProvince:ACT}")
    protected String orgStateProvince;
    @Value("${orgPostcode:2601}")
    protected String orgPostcode;
    @Value("${orgCountry:Australia}")
    protected String orgCountry;
    @Value("${orgPhone:+61 (0) 2 6246 4400}")
    protected String orgPhone;
    @Value("${orgFax:+61 (0) 2 6246 4400}")
    protected String orgFax;
    @Value("${orgEmail:support@ala.org.au}")
    protected String orgEmail;

    @Value("${service.bie.ws.url:http://bie.ala.org.au/ws}")
    protected String bieWebService;

    @Value("${service.bie.ui.url:http://bie.ala.org.au}")
    protected String bieUiUrl;

    /**
     * Threshold for caching a whole PointType for a query or only caching the current bounding box.
     */
    @Value("${wms.cache.maxLayerPoints:100000}")
    private int wmsCacheMaxLayerPoints;

    /**
     * Occurrence count where < uses pivot and > uses facet for retrieving points. Can be fine tuned with
     * multiple queries and comparing DEBUG *
     */
    @Value("${wms.facetPivotCutoff:2000}")
    private int wmsFacetPivotCutoff;

    //Stores query hashes + occurrence counts, and, query hashes + pointType + point counts
    private LRUMap countsCache = new LRUMap(10000);
    private Object countLock = new Object();

    static {
        byte[] b = null;
        try {
            RandomAccessFile raf = new RandomAccessFile(WMSController.class.getResource("/blank.png").getFile(),
                    "r");
            b = new byte[(int) raf.length()];
            raf.read(b);
            raf.close();
        } catch (Exception e) {
            logger.error("Unable to open blank image file");
        }
        blankImageBytes = b;
    }

    @Inject
    EhCacheManagerFactoryBean cacheManager;

    @RequestMapping(value = { "/webportal/params", "/mapping/params" }, method = RequestMethod.POST)
    public void storeParams(SpatialSearchRequestParams requestParams,
            @RequestParam(value = "bbox", required = false, defaultValue = "false") String bbox,
            @RequestParam(value = "title", required = false) String title,
            @RequestParam(value = "maxage", required = false, defaultValue = "-1") Long maxage,
            @RequestParam(value = "source", required = false) String source, HttpServletResponse response)
            throws Exception {

        //set default values for parameters not stored in the qid.
        requestParams.setFl("");
        requestParams.setFacets(new String[] {});
        requestParams.setStart(0);
        requestParams.setFacet(false);
        requestParams.setFlimit(0);
        requestParams.setPageSize(0);
        requestParams.setSort("score");
        requestParams.setDir("asc");
        requestParams.setFoffset(0);
        requestParams.setFprefix("");
        requestParams.setFsort("");
        requestParams.setIncludeMultivalues(false);

        //move qc to fq
        if (StringUtils.isNotEmpty(requestParams.getQc())) {
            queryFormatUtils.addFqs(new String[] { requestParams.getQc() }, requestParams);
            requestParams.setQc("");
        }

        //move lat/lon/radius to fq
        if (requestParams.getLat() != null) {
            String fq = queryFormatUtils.latLonPart(requestParams);
            queryFormatUtils.addFqs(new String[] { fq }, requestParams);
            requestParams.setLat(null);
            requestParams.setLon(null);
            requestParams.setRadius(null);
        }

        String qid = qidCacheDAO.generateQid(requestParams, bbox, title, maxage, source);
        if (qid == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                    "WKT provided has more than " + maxWktPoints + " points and failed to be simplified.");
        } else {
            response.setContentType("text/plain");
            writeBytes(response, qid.getBytes());
        }
    }

    /**
     * Test presence of query params {id} in params store.
     */
    @RequestMapping(value = { "/webportal/params/{id}", "/mapping/params/{id}" }, method = RequestMethod.GET)
    public @ResponseBody Boolean storeParams(@PathVariable("id") Long id) throws Exception {
        return qidCacheDAO.get(String.valueOf(id)) != null;
    }

    /**
     * Allows the details of a cached query to be viewed.
     *
     * @param id
     * @return
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/params/details/{id}",
            "/mapping/params/details/{id}" }, method = RequestMethod.GET)
    public @ResponseBody Qid getQid(@PathVariable("id") Long id) throws Exception {
        return qidCacheDAO.get(String.valueOf(id));
    }

    /**
     * JSON web service that returns a list of species and record counts for a given location search
     *
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/species", "/mapping/species" }, method = RequestMethod.GET)
    public @ResponseBody List<TaxaCountDTO> listSpecies(SpatialSearchRequestParams requestParams) throws Exception {
        return searchDAO.findAllSpecies(requestParams);
    }

    /**
     * List of species for webportal as csv.
     *
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/species.csv", "/mapping/species.csv" }, method = RequestMethod.GET)
    public void listSpeciesCsv(SpatialSearchRequestParams requestParams, HttpServletResponse response)
            throws Exception {

        List<TaxaCountDTO> list = searchDAO.findAllSpecies(requestParams);

        //format as csv
        StringBuilder sb = new StringBuilder();
        sb.append("Family,Scientific name,Common name,Taxon rank,LSID,# Occurrences");
        for (TaxaCountDTO d : list) {
            String family = d.getFamily();
            String name = d.getName();
            String commonName = d.getCommonName();
            String guid = d.getGuid();
            String rank = d.getRank();

            if (family == null) {
                family = "";
            }
            if (name == null) {
                name = "";
            }
            if (commonName == null) {
                commonName = "";
            }

            if (d.getGuid() == null) {
                //when guid is empty name contains name_lsid value.
                if (d.getName() != null) {
                    //parse name
                    String[] nameLsid = d.getName().split("\\|");
                    if (nameLsid.length >= 2) {
                        name = nameLsid[0];
                        guid = nameLsid[1];
                        rank = "scientific name";

                        if (nameLsid.length >= 3) {
                            commonName = nameLsid[2];
                        }
                        //                        if(nameLsid.length >= 4) {
                        //                            kingdom = nameLsid[3];
                        //                        }
                    } else {
                        name = NULL_NAME;
                    }
                }
            }
            if (d.getCount() != null && guid != null) {
                sb.append("\n\"").append(family.replace("\"", "\"\"").trim()).append("\",\"")
                        .append(name.replace("\"", "\"\"").trim()).append("\",\"")
                        .append(commonName.replace("\"", "\"\"").trim()).append("\",").append(rank).append(",")
                        .append(guid).append(",").append(d.getCount());
            }
        }

        writeBytes(response, sb.toString().getBytes("UTF-8"));
    }

    /**
     * Get legend for a query and facet field (colourMode).
     *
     * if "Accept" header is application/json return json otherwise
     *
     * @param requestParams
     * @param colourMode
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/legend", "/mapping/legend" }, method = RequestMethod.GET)
    @ResponseBody
    public List<LegendItem> legend(SpatialSearchRequestParams requestParams,
            @RequestParam(value = "cm", required = false, defaultValue = "") String colourMode,
            @RequestParam(value = "type", required = false, defaultValue = "application/csv") String returnType,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        String[] acceptableTypes = new String[] { "application/json", "application/csv" };

        String accepts = request.getHeader("Accept");
        //only allow a single format to be supplied in the header otherwise use the default returnType
        returnType = StringUtils.isNotEmpty(accepts) && !accepts.contains(",") ? accepts : returnType;
        if (!Arrays.asList(acceptableTypes).contains(returnType)) {
            response.sendError(response.SC_NOT_ACCEPTABLE,
                    "Unable to produce a legend in the supplied \"Accept\" format: " + returnType);
            return null;
        }
        boolean isCsv = returnType.equals("application/csv");
        //test for cutpoints on the back of colourMode
        String[] s = colourMode.split(",");
        String[] cutpoints = null;
        if (s.length > 1) {
            cutpoints = new String[s.length - 1];
            System.arraycopy(s, 1, cutpoints, 0, cutpoints.length);
        }
        requestParams.setFormattedQuery(null);
        List<LegendItem> legend = searchDAO.getLegend(requestParams, s[0], cutpoints);
        if (cutpoints == null) {
            java.util.Collections.sort(legend);
        }
        StringBuilder sb = new StringBuilder();
        if (isCsv) {
            sb.append("name,red,green,blue,count");
        }
        int i = 0;
        //add legend entries.
        int offset = 0;
        for (i = 0; i < legend.size(); i++) {
            LegendItem li = legend.get(i);
            String name = li.getName();
            if (StringUtils.isEmpty(name)) {
                name = NULL_NAME;
            }
            int colour = DEFAULT_COLOUR;
            if (cutpoints == null) {
                colour = ColorUtil.colourList[Math.min(i, ColorUtil.colourList.length - 1)];
            } else if (cutpoints != null && i - offset < cutpoints.length) {
                if (name.equals(NULL_NAME) || name.startsWith("-")) {
                    offset++;
                    colour = DEFAULT_COLOUR;
                } else {
                    colour = ColorUtil.getRangedColour(i - offset, cutpoints.length / 2);
                }
            }
            li.setRGB(colour);
            if (isCsv) {
                sb.append("\n\"").append(name.replace("\"", "\"\"")).append("\",").append(ColorUtil.getRGB(colour)) //repeat last colour if required
                        .append(",").append(legend.get(i).getCount());
            }
        }

        //now generate the JSON if necessary
        if (returnType.equals("application/json")) {
            return legend;
        } else {
            writeBytes(response, sb.toString().getBytes("UTF-8"));
            return null;
        }
    }

    /**
     * List data providers for a query.
     *
     * @param requestParams
     * @return
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/dataProviders", "/mapping/dataProviders" }, method = RequestMethod.GET)
    @ResponseBody
    public List<DataProviderCountDTO> queryInfo(SpatialSearchRequestParams requestParams) throws Exception {
        return searchDAO.getDataProviderList(requestParams);
    }

    /**
     * Get query bounding box as csv containing:
     * min longitude, min latitude, max longitude, max latitude
     *
     * @param requestParams
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/bbox", "/mapping/bbox" }, method = RequestMethod.GET)
    public void boundingBox(SpatialSearchRequestParams requestParams, HttpServletResponse response)
            throws Exception {

        double[] bbox = null;

        if (bbox == null) {
            bbox = searchDAO.getBBox(requestParams);
        }

        writeBytes(response, (bbox[0] + "," + bbox[1] + "," + bbox[2] + "," + bbox[3]).getBytes("UTF-8"));
    }

    /**
     * Get query bounding box as JSON array containing:
     * min longitude, min latitude, max longitude, max latitude
     *
     * @param requestParams
     * @param response
     * @return
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/bounds", "/mapping/bounds" }, method = RequestMethod.GET)
    public @ResponseBody double[] jsonBoundingBox(SpatialSearchRequestParams requestParams,
            HttpServletResponse response) throws Exception {

        double[] bbox = null;

        String q = requestParams.getQ();
        //when requestParams only contain a qid, get the bbox from the qidCache
        if (q.startsWith("qid:") && StringUtils.isEmpty(requestParams.getWkt())
                && (requestParams.getFq().length == 0
                        || (requestParams.getFq().length == 1 && StringUtils.isEmpty(requestParams.getFq()[0])))) {
            try {
                bbox = qidCacheDAO.get(q.substring(4)).getBbox();
            } catch (Exception e) {
            }
        }

        if (bbox == null) {
            bbox = searchDAO.getBBox(requestParams);
        }

        return bbox;
    }

    /**
     * Get occurrences by query as JSON.
     *
     * @param requestParams
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/occurrences*", "/mapping/occurrences*" }, method = RequestMethod.GET)
    @ResponseBody
    public SearchResultDTO occurrences(SpatialSearchRequestParams requestParams, Model model) throws Exception {

        SearchResultDTO searchResult = new SearchResultDTO();

        if (StringUtils.isEmpty(requestParams.getQ())) {
            return searchResult;
        }

        //searchUtils.updateSpatial(requestParams);
        searchResult = searchDAO.findByFulltextSpatialQuery(requestParams, null);
        model.addAttribute("searchResult", searchResult);

        if (logger.isDebugEnabled()) {
            logger.debug("Returning results set with: " + searchResult.getTotalRecords());
        }

        return searchResult;
    }

    /**
     * Get occurrences by query as gzipped csv.
     *
     * @param requestParams
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/occurrences.gz", "/mapping/occurrences.gz" }, method = RequestMethod.GET)
    public void occurrenceGz(SpatialSearchRequestParams requestParams, HttpServletResponse response)
            throws Exception {

        response.setContentType("text/plain");
        response.setCharacterEncoding("gzip");

        try {
            ServletOutputStream outStream = response.getOutputStream();
            java.util.zip.GZIPOutputStream gzip = new java.util.zip.GZIPOutputStream(outStream);

            writeOccurrencesCsvToStream(requestParams, gzip);

            gzip.flush();
            gzip.close();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    private void writeOccurrencesCsvToStream(SpatialSearchRequestParams requestParams, OutputStream stream)
            throws Exception {
        SolrDocumentList sdl = searchDAO.findByFulltext(requestParams);

        byte[] bComma = ",".getBytes("UTF-8");
        byte[] bNewLine = "\n".getBytes("UTF-8");
        byte[] bDblQuote = "\"".getBytes("UTF-8");

        if (sdl != null && sdl.size() > 0) {
            //header field identification
            ArrayList<String> header = new ArrayList<String>();
            if (requestParams.getFl() == null || requestParams.getFl().isEmpty()) {
                TreeSet<String> unique = new TreeSet<String>();
                for (int i = 0; i < sdl.size(); i++) {
                    unique.addAll(sdl.get(i).getFieldNames());
                }
                header = new ArrayList<String>(unique);
            } else {
                String[] fields = requestParams.getFl().split(",");
                for (int i = 0; i < fields.length; i++) {
                    if (fields[i].length() > 0) {
                        header.add(fields[i]);
                    }
                }
            }

            //write header
            for (int i = 0; i < header.size(); i++) {
                if (i > 0) {
                    stream.write(bComma);
                }
                stream.write(header.get(i).getBytes("UTF-8"));
            }

            //write records
            for (int i = 0; i < sdl.size(); i++) {
                stream.write(bNewLine);
                for (int j = 0; j < header.size(); j++) {
                    if (j > 0) {
                        stream.write(bComma);
                    }
                    if (sdl.get(i).containsKey(header.get(j))) {
                        stream.write(bDblQuote);
                        stream.write(String.valueOf(sdl.get(i).getFieldValue(header.get(j))).replace("\"", "\"\"")
                                .getBytes("UTF-8"));
                        stream.write(bDblQuote);
                    }
                }
            }
        }
    }

    private void writeBytes(HttpServletResponse response, byte[] bytes) throws IOException {
        try {
            response.setContentType("text/plain");
            response.setCharacterEncoding("UTF-8");
            ServletOutputStream outStream = response.getOutputStream();
            outStream.write(bytes);
            outStream.flush();
            outStream.close();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * 4326 to 900913 pixel and m conversion
     */
    private int map_offset = 268435456; // half the Earth's circumference at zoom level 21
    private double map_radius = map_offset / Math.PI;

    int convertLatToPixel(double lat) {
        return (int) Math.round(map_offset - map_radius
                * Math.log((1 + Math.sin(lat * Math.PI / 180)) / (1 - Math.sin(lat * Math.PI / 180))) / 2);
    }

    int convertLatToPixel4326(double lat, double top, double bottom, int pixelHeight) {
        return (int) (((lat - top) / (bottom - top)) * pixelHeight);
    }

    int convertLngToPixel4326(double lng, double left, double right, int pixelWidth) {
        return (int) (((lng - left) / (right - left)) * pixelWidth);
    }

    int convertLngToPixel(double lng) {
        return (int) Math.round(map_offset + map_radius * lng * Math.PI / 180);
    }

    double convertMetersToLng(double meters) {
        return meters / 20037508.342789244 * 180;
    }

    //    //http://mapsforge.googlecode.com/svn-history/r1841/trunk/mapsforge-map-writer/src/main/java/org/mapsforge/map/writer/model/MercatorProjection.java
    double convertLngToMeters(double lng) {
        return 6378137.0 * Math.PI / 180 * lng;
    }

    //
    //    public static final double WGS_84_EQUATORIALRADIUS = 6378137.0;
    double convertLatToMeters(double lat) {
        return 6378137.0 * Math.log(Math.tan(Math.PI / 180 * (45 + lat / 2.0)));
    }

    double convertMetersToLat(double meters) {
        return 180.0 / Math.PI * (2 * Math.atan(Math.exp(meters / 20037508.342789244 * Math.PI)) - Math.PI / 2.0);
    }

    /**
     * Map a zoom level to a coordinate accuracy level
     *
     * @return
     */
    protected PointType getPointTypeForDegreesPerPixel(double resolution) {
        PointType pointType = null;
        // Map zoom levels to lat/long accuracy levels
        if (resolution >= 1) {
            pointType = PointType.POINT_1;
        } else if (resolution >= 0.1) {
            pointType = PointType.POINT_01;
        } else if (resolution >= 0.01) {
            pointType = PointType.POINT_001;
        } else if (resolution >= 0.001) {
            pointType = PointType.POINT_0001;
        } else if (resolution >= 0.0001) {
            pointType = PointType.POINT_00001;
        } else {
            pointType = PointType.POINT_RAW;
        }
        return pointType;
    }

    void displayBlankImage(HttpServletResponse response) {
        try {
            ServletOutputStream outStream = response.getOutputStream();
            outStream.write(blankImageBytes);
            outStream.flush();
            outStream.close();
        } catch (Exception e) {
            logger.error("Unable to write image", e);
        }
    }

    /**
     * @param bboxString
     * @param width
     * @param height
     * @param size
     * @param uncertainty
     * @param mbbox       the mbbox to initialise
     * @param bbox        the bbox to initialise
     * @param pbbox       the pbbox to initialise
     * @param tilebbox    the tilebbox to initialise
     * @return
     */
    private double getBBoxes(String bboxString, int width, int height, int size, boolean uncertainty,
            double[] mbbox, double[] bbox, double[] pbbox, double[] tilebbox) {
        String[] splitBBox = bboxString.split(",");
        for (int i = 0; i < 4; i++) {
            boolean errorInThisPosition = false;
            try {
                if (splitBBox.length > i) {
                    tilebbox[i] = Double.parseDouble(splitBBox[i]);
                    mbbox[i] = tilebbox[i];
                } else {
                    logger.error("Problem parsing BBOX: '" + bboxString + "' at position " + i);
                    errorInThisPosition = true;
                }
            } catch (Exception e) {
                logger.error("Problem parsing BBOX: '" + bboxString + "' at position " + i, e);
                errorInThisPosition = true;
            }
            if (errorInThisPosition) {
                tilebbox[i] = 0.0d;
                mbbox[i] = 0.0d;
            }
        }

        //adjust bbox extents with half pixel width/height
        double pixelWidth = (mbbox[2] - mbbox[0]) / width;
        double pixelHeight = (mbbox[3] - mbbox[1]) / height;
        mbbox[0] += pixelWidth / 2;
        mbbox[2] -= pixelWidth / 2;
        mbbox[1] += pixelHeight / 2;
        mbbox[3] -= pixelHeight / 2;

        //offset for points bounding box by dot size
        double xoffset = (mbbox[2] - mbbox[0]) / (double) width * size;
        double yoffset = (mbbox[3] - mbbox[1]) / (double) height * size;

        //adjust offset for pixel height/width
        xoffset += pixelWidth;
        yoffset += pixelHeight;

        pbbox[0] = convertLngToPixel(convertMetersToLng(mbbox[0]));
        pbbox[1] = convertLatToPixel(convertMetersToLat(mbbox[1]));
        pbbox[2] = convertLngToPixel(convertMetersToLng(mbbox[2]));
        pbbox[3] = convertLatToPixel(convertMetersToLat(mbbox[3]));

        bbox[0] = convertMetersToLng(mbbox[0] - xoffset);
        bbox[1] = convertMetersToLat(mbbox[1] - yoffset);
        bbox[2] = convertMetersToLng(mbbox[2] + xoffset);
        bbox[3] = convertMetersToLat(mbbox[3] + yoffset);

        double degreesPerPixel = Math.min(
                (convertMetersToLng(mbbox[2]) - convertMetersToLng(mbbox[0])) / (double) width,
                (convertMetersToLng(mbbox[3]) - convertMetersToLng(mbbox[1])) / (double) height);
        return degreesPerPixel;
    }

    /**
     * @param bboxString
     * @param width
     * @param height
     * @param size
     * @param uncertainty
     * @param mbbox       the mbbox to initialise
     * @param bbox        the bbox to initialise
     * @param pbbox       the pbbox to initialise
     * @return
     */
    private double getBBoxes4326(String bboxString, int width, int height, int size, boolean uncertainty,
            double[] mbbox, double[] bbox, double[] pbbox, double[] tilebbox) {
        int i = 0;
        for (String s : bboxString.split(",")) {
            try {
                tilebbox[i] = Double.parseDouble(s);
                mbbox[i] = tilebbox[i];
                i++;
            } catch (Exception e) {
                logger.error("Problem parsing BBOX: '" + bboxString + "'", e);
            }
        }

        //adjust bbox extents with half pixel width/height
        double pixelWidth = (mbbox[2] - mbbox[0]) / width;
        double pixelHeight = (mbbox[3] - mbbox[1]) / height;
        mbbox[0] += pixelWidth / 2;
        mbbox[2] -= pixelWidth / 2;
        mbbox[1] += pixelHeight / 2;
        mbbox[3] -= pixelHeight / 2;

        //offset for points bounding box by dot size
        double xoffset = (mbbox[2] - mbbox[0]) / (double) width * size;
        double yoffset = (mbbox[3] - mbbox[1]) / (double) height * size;

        //adjust offset for pixel height/width
        xoffset += pixelWidth;
        yoffset += pixelHeight;

        //actual bounding box
        bbox[0] = mbbox[0] - xoffset;
        bbox[1] = mbbox[1] - yoffset;
        bbox[2] = mbbox[2] + xoffset;
        bbox[3] = mbbox[3] + yoffset;

        double degreesPerPixel = Math.min(pixelWidth, pixelHeight);
        return degreesPerPixel;
    }

    private String getQ(String cql_filter) {
        String q = cql_filter;
        int p1 = cql_filter.indexOf("qid:");
        if (p1 >= 0) {
            int p2 = cql_filter.indexOf('&', p1 + 1);
            if (p2 < 0) {
                p2 = cql_filter.indexOf(';', p1 + 1);
            }
            if (p2 < 0) {
                p2 = cql_filter.length();
            }
            q = cql_filter.substring(p1, p2);
        }
        return q;
    }

    private String convertBBox4326To900913(String bbox) {
        int i = 0;
        Double[] mbbox = new Double[4];
        for (String s : bbox.split(",")) {
            if (i % 2 == 0)
                mbbox[i] = convertLngToMeters(Double.parseDouble(s));
            else
                mbbox[i] = convertLatToMeters(Double.parseDouble(s));
            i++;
        }
        return StringUtils.join(mbbox, ",");
    }

    // add this to the GetCapabilities...
    @RequestMapping(value = { "/ogc/getMetadata" }, method = RequestMethod.GET)
    public String getMetadata(@RequestParam(value = "LAYER", required = false, defaultValue = "") String layer,
            @RequestParam(value = "q", required = false, defaultValue = "") String query,
            HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {

        String taxonName = "";
        String rank = "";
        String q = "";
        if (StringUtils.trimToNull(layer) != null) {
            String[] parts = layer.split(":");
            taxonName = parts[parts.length - 1];
            if (parts.length > 1) {
                rank = parts[0];
            }
            q = layer;
        } else if (StringUtils.trimToNull(query) != null) {
            String[] parts = query.split(":");
            taxonName = parts[parts.length - 1];
            if (parts.length > 1) {
                rank = parts[0];
            }
            q = query;
        } else {
            response.sendError(400);
        }

        ObjectMapper om = new ObjectMapper();
        String guid = null;
        JsonNode guidLookupNode = om
                .readTree(new URL(bieWebService + "/guid/" + URLEncoder.encode(taxonName, "UTF-8")));
        //NC: Fixed the ArraryOutOfBoundsException when the lookup fails to yield a result
        if (guidLookupNode.isArray() && guidLookupNode.size() > 0) {
            JsonNode idNode = guidLookupNode.get(0).get("acceptedIdentifier");//NC: changed to used the acceptedIdentifier because this will always hold the guid for the accepted taxon concept whether or not a synonym name is provided
            guid = idNode != null ? idNode.asText() : null;
        }
        String newQuery = "raw_name:" + taxonName;
        if (guid != null) {

            model.addAttribute("guid", guid);
            model.addAttribute("speciesPageUrl", bieUiUrl + "/species/" + guid);
            JsonNode node = om.readTree(new URL(bieWebService + "/species/info/" + guid + ".json"));
            JsonNode tc = node.get("taxonConcept");
            JsonNode imageNode = tc.get("smallImageUrl");
            String imageUrl = imageNode != null ? imageNode.asText() : null;
            if (imageUrl != null) {
                model.addAttribute("imageUrl", imageUrl);
                JsonNode imageMetadataNode = node.get("taxonConcept").get("imageMetadataUrl");
                String imageMetadataUrl = imageMetadataNode != null ? imageMetadataNode.asText() : null;

                //image metadata
                JsonNode imageMetadata = om.readTree(new URL(imageMetadataUrl));
                if (imageMetadata != null) {
                    if (imageMetadata.get("http://purl.org/dc/elements/1.1/creator") != null)
                        model.addAttribute("imageCreator",
                                imageMetadata.get("http://purl.org/dc/elements/1.1/creator").asText());
                    if (imageMetadata.get("http://purl.org/dc/elements/1.1/license") != null)
                        model.addAttribute("imageLicence",
                                imageMetadata.get("http://purl.org/dc/elements/1.1/license").asText());
                    if (imageMetadata.get("http://purl.org/dc/elements/1.1/source") != null)
                        model.addAttribute("imageSource",
                                imageMetadata.get("http://purl.org/dc/elements/1.1/source").asText());
                }
            }

            JsonNode leftNode = tc.get("left");
            JsonNode rightNode = tc.get("right");
            newQuery = leftNode != null && rightNode != null
                    ? "lft:[" + leftNode.asText() + " TO " + rightNode.asText() + "]"
                    : "taxon_concept_lsid:" + guid;
            logger.debug("The new query : " + newQuery);

            //common name
            JsonNode commonNameNode = tc.get("commonNameSingle");
            if (commonNameNode != null) {
                model.addAttribute("commonName", commonNameNode.asText());
                logger.debug("retrieved name: " + commonNameNode.asText());
            }

            //name
            JsonNode nameNode = tc.get("nameComplete");
            if (nameNode != null) {
                model.addAttribute("name", nameNode.asText());
                logger.debug("retrieved name: " + nameNode.asText());
            }

            //authorship
            JsonNode authorshipNode = node.get("taxonConcept").get("author");
            if (authorshipNode != null)
                model.addAttribute("authorship", authorshipNode.asText());

            //taxonomic information
            JsonNode node2 = om.readTree(new URL(bieWebService + "/species/" + guid + ".json"));
            JsonNode classificationNode = node2.get("classification");
            model.addAttribute("kingdom",
                    StringUtils.capitalize(classificationNode.get("kingdom").asText().toLowerCase()));
            model.addAttribute("phylum",
                    StringUtils.capitalize(classificationNode.get("phylum").asText().toLowerCase()));
            model.addAttribute("clazz",
                    StringUtils.capitalize(classificationNode.get("clazz").asText().toLowerCase()));
            model.addAttribute("order",
                    StringUtils.capitalize(classificationNode.get("order").asText().toLowerCase()));
            model.addAttribute("family",
                    StringUtils.capitalize(classificationNode.get("family").asText().toLowerCase()));
            model.addAttribute("genus", classificationNode.get("genus").asText());

            JsonNode taxonNameNode = node2.get("taxonName");
            if (taxonNameNode != null && taxonNameNode.get("specificEpithet") != null) {
                model.addAttribute("specificEpithet", taxonNameNode.get("specificEpithet").asText());
            }
        }

        SpatialSearchRequestParams searchParams = new SpatialSearchRequestParams();
        searchParams.setQ(newQuery);
        searchParams.setFacets(new String[] { "data_resource" });
        searchParams.setPageSize(0);
        List<FacetResultDTO> facets = searchDAO.getFacetCounts(searchParams);
        model.addAttribute("query", newQuery); //need a facet on data providers
        model.addAttribute("dataProviders", facets.get(0).getFieldResult()); //need a facet on data providers
        return "metadata/mcp";
    }

    @RequestMapping(value = { "/ogc/getFeatureInfo" }, method = RequestMethod.GET)
    public String getFeatureInfo(
            @RequestParam(value = "CQL_FILTER", required = false, defaultValue = "") String cql_filter,
            @RequestParam(value = "ENV", required = false, defaultValue = "") String env,
            @RequestParam(value = "BBOX", required = true, defaultValue = "0,-90,180,0") String bboxString,
            @RequestParam(value = "WIDTH", required = true, defaultValue = "256") Integer width,
            @RequestParam(value = "HEIGHT", required = true, defaultValue = "256") Integer height,
            @RequestParam(value = "STYLES", required = false, defaultValue = "") String styles,
            @RequestParam(value = "SRS", required = false, defaultValue = "") String srs,
            @RequestParam(value = "QUERY_LAYERS", required = false, defaultValue = "") String queryLayers,
            @RequestParam(value = "X", required = true, defaultValue = "0") Double x,
            @RequestParam(value = "Y", required = true, defaultValue = "0") Double y, HttpServletRequest request,
            HttpServletResponse response, Model model) throws Exception {

        logger.debug("WMS - GetFeatureInfo requested for: " + queryLayers);

        if ("EPSG:4326".equals(srs))
            bboxString = convertBBox4326To900913(bboxString); // to work around a UDIG bug

        WmsEnv vars = new WmsEnv(env, styles);
        double[] mbbox = new double[4];
        double[] bbox = new double[4];
        double[] pbbox = new double[4];
        double[] tilebbox = new double[4];
        int size = vars.size + (vars.highlight != null ? HIGHLIGHT_RADIUS * 2 + (int) (vars.size * 0.2) : 0) + 5; //bounding box buffer

        //what is the size of the dot in degrees
        double resolution = getBBoxes(bboxString, width, height, size, vars.uncertainty, mbbox, bbox, pbbox,
                tilebbox);

        //resolution should be a value < 1
        PointType pointType = getPointTypeForDegreesPerPixel(resolution);

        double longitude = bbox[0] + (((bbox[2] - bbox[0]) / width) * x);
        double latitude = bbox[3] - (((bbox[3] - bbox[1]) / height) * y);

        //round to the correct point size
        double roundedLongitude = pointType.roundToPointType(longitude);
        double roundedLatitude = pointType.roundToPointType(latitude);

        //get the pixel size of the circles
        double minLng = pointType.roundDownToPointType(roundedLongitude - (pointType.getValue() * 2 * (size + 3)));
        double maxLng = pointType.roundUpToPointType(roundedLongitude + (pointType.getValue() * 2 * (size + 3)));
        double minLat = pointType.roundDownToPointType(roundedLatitude - (pointType.getValue() * 2 * (size + 3)));
        double maxLat = pointType.roundUpToPointType(roundedLatitude + (pointType.getValue() * 2 * (size + 3)));

        //do the SOLR query
        SpatialSearchRequestParams requestParams = new SpatialSearchRequestParams();
        String q = convertLayersParamToQ(queryLayers);
        requestParams.setQ(convertLayersParamToQ(queryLayers)); //need to derive this from the layer name
        logger.debug("WMS GetFeatureInfo for " + queryLayers + ", longitude:[" + minLng + " TO " + maxLng
                + "],  latitude:[" + minLat + " TO " + maxLat + "]");

        String[] fqs = new String[] { "longitude:[" + minLng + " TO " + maxLng + "]",
                "latitude:[" + minLat + " TO " + maxLat + "]" };
        requestParams.setFq(fqs);
        //requestParams.setFq(new String[]{"point-"+pointType.getValue()+":"+roundedLatitude+","+roundedLongitude});
        requestParams.setFacet(false);

        //TODO: paging
        SolrDocumentList sdl = searchDAO.findByFulltext(requestParams);
        //send back the results.
        String body = "";
        if (sdl != null && sdl.size() > 0) {
            SolrDocument doc = sdl.get(0);
            model.addAttribute("record", doc.getFieldValueMap());
            model.addAttribute("totalRecords", sdl.getNumFound());
        }

        model.addAttribute("uriUrl",
                "http://biocache.ala.org.au/occurrences/search?q=" + URLEncoder.encode(q, "UTF-8") + "&fq="
                        + URLEncoder.encode(fqs[0], "UTF-8") + "&fq=" + URLEncoder.encode(fqs[1], "UTF-8"));

        model.addAttribute("pointType", pointType.name());
        model.addAttribute("minLng", minLng);
        model.addAttribute("maxLng", maxLng);
        model.addAttribute("minLat", minLat);
        model.addAttribute("maxLat", maxLat);
        model.addAttribute("latitudeClicked", latitude);
        model.addAttribute("longitudeClicked", longitude);

        return "metadata/getFeatureInfo";
    }

    String convertLayersParamToQ(String layers) {
        if (StringUtils.trimToNull(layers) != null) {
            String[] parts = layers.split(",");
            String[] formattedParts = new String[parts.length];
            int i = 0;
            for (String part : parts) {
                if (part.contains(":")) {
                    formattedParts[i] = part.replace('_', ' ').replace(":", ":\"") + "\"";
                } else if (part.startsWith("\"")) {
                    formattedParts[i] = "\"" + part + "\"";
                } else {
                    formattedParts[i] = part;
                }
                i++;
            }
            return StringUtils.join(formattedParts, " OR ");
        } else {
            return null;
        }
    }

    @RequestMapping(value = { "/ogc/legendGraphic" }, method = RequestMethod.GET)
    public void getLegendGraphic(@RequestParam(value = "ENV", required = false, defaultValue = "") String env,
            @RequestParam(value = "STYLE", required = false, defaultValue = "8b0000;opacity=1;size=5") String style,
            @RequestParam(value = "WIDTH", required = false, defaultValue = "30") Integer width,
            @RequestParam(value = "HEIGHT", required = false, defaultValue = "20") Integer height,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        try {
            if (StringUtils.trimToNull(env) == null && StringUtils.trimToNull(style) == null) {
                style = "8b0000;opacity=1;size=5";
            }

            WmsEnv wmsEnv = new WmsEnv(env, style);
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g = (Graphics2D) img.getGraphics();
            int size = width > height ? height : width;
            Paint fill = new Color(wmsEnv.colour | wmsEnv.alpha << 24);
            g.setPaint(fill);
            g.fillOval(0, 0, size, size);
            OutputStream out = response.getOutputStream();
            logger.debug("WMS - GetLegendGraphic requested : " + request.getQueryString());
            response.setContentType("image/png");
            ImageIO.write(img, "png", out);
            out.close();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * Returns a get capabilities response by default.
     *
     * @param requestParams
     * @param cql_filter
     * @param env
     * @param srs
     * @param styles
     * @param style
     * @param bboxString
     * @param width
     * @param height
     * @param cache
     * @param requestString
     * @param outlinePoints
     * @param outlineColour
     * @param layers
     * @param query
     * @param filterQueries
     * @param x
     * @param y
     * @param spatiallyValidOnly
     * @param marineOnly
     * @param terrestrialOnly
     * @param limitToFocus
     * @param useSpeciesGroups
     * @param request
     * @param response
     * @param model
     * @throws Exception
     */
    @RequestMapping(value = { "/ogc/ows", "/ogc/capabilities" }, method = RequestMethod.GET)
    public void getCapabilities(SpatialSearchRequestParams requestParams,
            @RequestParam(value = "CQL_FILTER", required = false, defaultValue = "") String cql_filter,
            @RequestParam(value = "ENV", required = false, defaultValue = "") String env,
            @RequestParam(value = "SRS", required = false, defaultValue = "EPSG:900913") String srs, //default to google mercator
            @RequestParam(value = "STYLES", required = false, defaultValue = "") String styles,
            @RequestParam(value = "STYLE", required = false, defaultValue = "") String style,
            @RequestParam(value = "BBOX", required = false, defaultValue = "") String bboxString,
            @RequestParam(value = "WIDTH", required = false, defaultValue = "256") Integer width,
            @RequestParam(value = "HEIGHT", required = false, defaultValue = "256") Integer height,
            @RequestParam(value = "CACHE", required = false, defaultValue = "default") String cache,
            @RequestParam(value = "REQUEST", required = false, defaultValue = "") String requestString,
            @RequestParam(value = "OUTLINE", required = false, defaultValue = "false") boolean outlinePoints,
            @RequestParam(value = "OUTLINECOLOUR", required = false, defaultValue = "0x000000") String outlineColour,
            @RequestParam(value = "LAYERS", required = false, defaultValue = "") String layers,
            @RequestParam(value = "q", required = false, defaultValue = "*:*") String query,
            @RequestParam(value = "fq", required = false) String[] filterQueries,
            @RequestParam(value = "X", required = true, defaultValue = "0") Double x,
            @RequestParam(value = "Y", required = true, defaultValue = "0") Double y,
            @RequestParam(value = "spatiallyValidOnly", required = false, defaultValue = "true") boolean spatiallyValidOnly,
            @RequestParam(value = "marineSpecies", required = false, defaultValue = "false") boolean marineOnly,
            @RequestParam(value = "terrestrialSpecies", required = false, defaultValue = "false") boolean terrestrialOnly,
            @RequestParam(value = "limitToFocus", required = false, defaultValue = "true") boolean limitToFocus,
            @RequestParam(value = "useSpeciesGroups", required = false, defaultValue = "false") boolean useSpeciesGroups,
            @RequestParam(value = "GRIDDETAIL", required = false, defaultValue = "16") int gridDivisionCount,
            HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {

        if ("GetMap".equalsIgnoreCase(requestString)) {
            generateWmsTile(requestParams, cql_filter, env, srs, styles, bboxString, width, height, cache,
                    requestString, outlinePoints, outlineColour, layers, null, gridDivisionCount, request,
                    response);
            return;
        }

        if ("GetLegendGraphic".equalsIgnoreCase(requestString)) {
            getLegendGraphic(env, style, 30, 20, request, response);
            return;
        }

        if ("GetFeatureInfo".equalsIgnoreCase(requestString)) {
            getFeatureInfo(cql_filter, env, bboxString, width, height, styles, srs, layers, x, y, request, response,
                    model);
            return;
        }

        //add the get capabilities request

        response.setContentType("text/xml");
        response.setHeader("Content-Description", "File Transfer");
        response.setHeader("Content-Disposition", "attachment; filename=GetCapabilities.xml");
        response.setHeader("Content-Transfer-Encoding", "binary");
        try {
            //webservicesRoot
            String biocacheServerUrl = baseWsUrl;
            PrintWriter writer = response.getWriter();
            writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE WMT_MS_Capabilities SYSTEM \""
                    + geoserverUrl + "/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd\">\n"
                    + "<WMT_MS_Capabilities version=\"1.1.1\" updateSequence=\"28862\">\n" + "  <Service>\n"
                    + "    <Name>OGC:WMS</Name>\n" + "    <Title>" + organizationName
                    + "(WMS) - Species occurrences</Title>\n"
                    + "    <Abstract>WMS services for species occurrences.</Abstract>\n" + "    <KeywordList>\n"
                    + "      <Keyword>WMS</Keyword>\n" + "      <Keyword>Species occurrence data</Keyword>\n"
                    + "      <Keyword>ALA</Keyword>\n" + "      <Keyword>CRIS</Keyword>\n" + "    </KeywordList>\n"
                    + "    <OnlineResource xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:type=\"simple\" xlink:href=\""
                    + biocacheServerUrl + "/ogc/wms\"/>\n" + "    <ContactInformation>\n"
                    + "      <ContactPersonPrimary>\n" + "        <ContactPerson>ALA Support</ContactPerson>\n"
                    + "        <ContactOrganization>" + organizationName + "</ContactOrganization>\n"
                    + "      </ContactPersonPrimary>\n"
                    + "      <ContactPosition>Support Manager</ContactPosition>\n" + "      <ContactAddress>\n"
                    + "        <AddressType></AddressType>\n" + "        <Address/>\n" + "        <City>" + orgCity
                    + "</City>\n" + "        <StateOrProvince>" + orgStateProvince + "</StateOrProvince>\n"
                    + "        <PostCode>" + orgPostcode + "</PostCode>\n" + "        <Country>" + orgCountry
                    + "</Country>\n" + "      </ContactAddress>\n" + "      <ContactVoiceTelephone>" + orgPhone
                    + "</ContactVoiceTelephone>\n" + "      <ContactFacsimileTelephone>" + orgFax
                    + "</ContactFacsimileTelephone>\n" + "      <ContactElectronicMailAddress>" + orgEmail
                    + "</ContactElectronicMailAddress>\n" + "    </ContactInformation>\n"
                    + "    <Fees>NONE</Fees>\n" + "    <AccessConstraints>NONE</AccessConstraints>\n"
                    + "  </Service>\n" + "  <Capability>\n" + "    <Request>\n" + "      <GetCapabilities>\n"
                    + "        <Format>application/vnd.ogc.wms_xml</Format>\n" + "        <DCPType>\n"
                    + "          <HTTP>\n" + "            <Get>\n"
                    + "              <OnlineResource xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:type=\"simple\" xlink:href=\""
                    + baseWsUrl + "/ogc/capabilities?SERVICE=WMS&amp;\"/>\n" + "            </Get>\n"
                    + "            <Post>\n"
                    + "              <OnlineResource xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:type=\"simple\" xlink:href=\""
                    + baseWsUrl + "/ogc/capabilities?SERVICE=WMS&amp;\"/>\n" + "            </Post>\n"
                    + "          </HTTP>\n" + "        </DCPType>\n" + "      </GetCapabilities>\n"
                    + "      <GetMap>\n" + "        <Format>image/png</Format>\n" + "        <DCPType>\n"
                    + "          <HTTP>\n" + "            <Get>\n"
                    + "              <OnlineResource xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:type=\"simple\" xlink:href=\""
                    + baseWsUrl + "/ogc/wms/reflect?SERVICE=WMS&amp;OUTLINE=TRUE&amp;\"/>\n"
                    + "            </Get>\n" + "          </HTTP>\n" + "        </DCPType>\n" + "      </GetMap>\n"
                    + "      <GetFeatureInfo>\n" + "        <Format>text/html</Format>\n" + "        <DCPType>\n"
                    + "          <HTTP>\n" + "            <Get>\n"
                    + "              <OnlineResource xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:type=\"simple\" xlink:href=\""
                    + baseWsUrl + "/ogc/getFeatureInfo\"/>\n" + "            </Get>\n" + "            <Post>\n"
                    + "              <OnlineResource xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:type=\"simple\" xlink:href=\""
                    + baseWsUrl + "/ogc/getFeatureInfo\"/>\n" + "            </Post>\n" + "          </HTTP>\n"
                    + "        </DCPType>\n" + "      </GetFeatureInfo>\n" + "      <GetLegendGraphic>\n"
                    + "        <Format>image/png</Format>\n" + "        <Format>image/jpeg</Format>\n"
                    + "        <Format>image/gif</Format>\n" + "        <DCPType>\n" + "          <HTTP>\n"
                    + "            <Get>\n"
                    + "              <OnlineResource xmlns:xlink=\"http://www.w3.org/1999/xlink\" xlink:type=\"simple\" xlink:href=\""
                    + baseWsUrl + "/ogc/legendGraphic\"/>\n" + "            </Get>\n" + "          </HTTP>\n"
                    + "        </DCPType>\n" + "      </GetLegendGraphic>\n" + "    </Request>\n"
                    + "    <Exception>\n" + "      <Format>application/vnd.ogc.se_xml</Format>\n"
                    + "      <Format>application/vnd.ogc.se_inimage</Format>\n" + "    </Exception>\n"
                    + "    <Layer>\n" + "      <Title>" + organizationName
                    + " - Species occurrence layers</Title>\n" + "      <Abstract>Custom WMS services for "
                    + organizationName + " species occurrences</Abstract>\n" + "      <SRS>EPSG:900913</SRS>\n"
                    + "      <SRS>EPSG:4326</SRS>\n"
                    + "     <LatLonBoundingBox minx=\"-179.9\" miny=\"-89.9\" maxx=\"179.9\" maxy=\"89.9\"/>\n");

            writer.write(generateStylesForPoints());

            if (spatiallyValidOnly) {
                filterQueries = org.apache.commons.lang3.ArrayUtils.add(filterQueries, "geospatial_kosher:true");
            }

            if (marineOnly) {
                filterQueries = org.apache.commons.lang3.ArrayUtils.add(filterQueries,
                        "species_habitats:Marine OR species_habitats:\"Marine and Non-marine\"");
            }

            if (terrestrialOnly) {
                filterQueries = org.apache.commons.lang3.ArrayUtils.add(filterQueries,
                        "species_habitats:\"Non-marine\" OR species_habitats:Limnetic");
            }

            if (limitToFocus) {
                //TODO retrieve focus from config file
                filterQueries = org.apache.commons.lang3.ArrayUtils.add(filterQueries,
                        "latitude:[-89 TO -8] AND longitude:[100 TO 165]");
            }

            query = searchUtils.convertRankAndName(query);
            logger.debug("GetCapabilities query in use: " + query);

            if (useSpeciesGroups) {
                taxonDAO.extractBySpeciesGroups(baseWsUrl + "/ogc/getMetadata", query, filterQueries, writer);
            } else {
                taxonDAO.extractHierarchy(baseWsUrl + "/ogc/getMetadata", query, filterQueries, writer);
            }

            writer.write("</Layer></Capability></WMT_MS_Capabilities>\n");

        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    public String generateStylesForPoints() {
        //need a better listings of colours
        String[] sizes = new String[] { "5", "10", "2" };
        String[] sizesNames = new String[] { "medium", "large", "small" };
        String[] opacities = new String[] { "0.5", "1", "0.2" };
        String[] opacitiesNames = new String[] { "medium", "opaque", "transparency" };
        StringBuffer sb = new StringBuffer();
        int colorIdx = 0;
        int sizeIdx = 0;
        int opIdx = 0;
        for (String color : ColorUtil.colorsNames) {
            for (String size : sizes) {
                for (String opacity : opacities) {
                    sb.append("<Style>\n" + "<Name>" + ColorUtil.colorsCodes[colorIdx] + ";opacity=" + opacity
                            + ";size=" + size + "</Name> \n" + "<Title>" + color + ";opacity="
                            + opacitiesNames[opIdx] + ";size=" + sizesNames[sizeIdx] + "</Title> \n"
                            + "</Style>\n");
                    opIdx++;
                }
                opIdx = 0;
                sizeIdx++;
            }
            sizeIdx = 0;
            colorIdx++;
        }
        return sb.toString();
    }

    @RequestMapping(value = { "/webportal/wms/clearCache", "/ogc/wms/clearCache",
            "/mapping/wms/clearCache" }, method = RequestMethod.GET)
    public void clearWMSCache(HttpServletResponse response, @RequestParam(value = "apiKey") String apiKey)
            throws Exception {
        if (isValidKey(apiKey)) {
            wmsCache.empty();
            response.setStatus(200);
        } else {
            response.setStatus(401);
        }
    }

    /**
     * WMS service for webportal.
     *
     * @param cql_filter q value.
     * @param env        ';' delimited field:value pairs.  See Env
     * @param bboxString
     * @param width
     * @param height
     * @param cache      'on' = use cache, 'off' = do not use cache this
     *                   also removes any related cache data.
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/wms/reflect", "/ogc/wms/reflect",
            "/mapping/wms/reflect" }, method = RequestMethod.GET)
    public void generateWmsTile(SpatialSearchRequestParams requestParams,
            @RequestParam(value = "CQL_FILTER", required = false, defaultValue = "") String cql_filter,
            @RequestParam(value = "ENV", required = false, defaultValue = "") String env,
            @RequestParam(value = "SRS", required = false, defaultValue = "EPSG:900913") String srs, //default to google mercator
            @RequestParam(value = "STYLES", required = false, defaultValue = "") String styles,
            @RequestParam(value = "BBOX", required = true, defaultValue = "") String bboxString,
            @RequestParam(value = "WIDTH", required = true, defaultValue = "256") Integer width,
            @RequestParam(value = "HEIGHT", required = true, defaultValue = "256") Integer height,
            @RequestParam(value = "CACHE", required = true, defaultValue = "default") String cache,
            @RequestParam(value = "REQUEST", required = true, defaultValue = "") String requestString,
            @RequestParam(value = "OUTLINE", required = true, defaultValue = "false") boolean outlinePoints,
            @RequestParam(value = "OUTLINECOLOUR", required = true, defaultValue = "0x000000") String outlineColour,
            @RequestParam(value = "LAYERS", required = false, defaultValue = "") String layers,
            @RequestParam(value = "HQ", required = false) String[] hqs,
            @RequestParam(value = "GRIDDETAIL", required = false, defaultValue = "16") Integer gridDivisionCount,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        //correct cache value
        if ("default".equals(cache))
            cache = wmsCacheEnabled ? "on" : "off";

        //Some WMS clients are ignoring sections of the GetCapabilities....
        if ("GetLegendGraphic".equalsIgnoreCase(requestString)) {
            getLegendGraphic(env, styles, 30, 20, request, response);
            return;
        }

        Set<Integer> hq = new HashSet<Integer>();
        if (hqs != null && hqs.length > 0) {
            for (String h : hqs) {
                hq.add(Integer.parseInt(h));
            }
        }

        logger.debug("WMS tile: " + request.getQueryString());

        response.setHeader("Cache-Control", "max-age=86400"); //age == 1 day
        response.setContentType("image/png"); //only png images generated

        boolean is4326 = false;
        WmsEnv vars = new WmsEnv(env, styles);
        double[] mbbox = new double[4];
        double[] bbox = new double[4];
        double[] pbbox = new double[4];
        double[] tilebbox = new double[4];

        //bbox adjustment for WMSCache is better with a stepped size
        int steppedSize = (int) (Math.ceil(vars.size / 20.0) * 20);
        int size = steppedSize + (vars.highlight != null ? HIGHLIGHT_RADIUS * 2 + (int) (steppedSize * 0.2) : 0)
                + 5; //bounding box buffer

        double resolution;
        if ("EPSG:4326".equals(srs)) {
            is4326 = true;
            //bboxString = convertBBox4326To900913(bboxString);    // to work around a UDIG bug

            resolution = getBBoxes4326(bboxString, width, height, size, vars.uncertainty, mbbox, bbox, pbbox,
                    tilebbox);
        } else {
            resolution = getBBoxes(bboxString, width, height, size, vars.uncertainty, mbbox, bbox, pbbox, tilebbox);
        }

        PointType pointType = getPointTypeForDegreesPerPixel(resolution);
        logger.debug("Rendering: " + pointType.name());

        String q = "";

        //CQL Filter takes precedence of the layer
        if (StringUtils.trimToNull(cql_filter) != null) {
            q = getQ(cql_filter);
        } else if (StringUtils.trimToNull(layers) != null && !"ALA:Occurrences".equalsIgnoreCase(layers)) {
            q = convertLayersParamToQ(layers);
        }

        String[] boundingBoxFqs = new String[2];
        boundingBoxFqs[0] = String.format("longitude:[%f TO %f]", bbox[0], bbox[2]);
        boundingBoxFqs[1] = String.format("latitude:[%f TO %f]", bbox[1], bbox[3]);

        int pointWidth = vars.size * 2;
        double width_mult = (width / (pbbox[2] - pbbox[0]));
        double height_mult = (height / (pbbox[1] - pbbox[3]));

        //build request
        if (q.length() > 0) {
            requestParams.setQ(q);
        } else {
            q = requestParams.getQ();
        }

        //bounding box test (requestParams must be 'qid:' + number only)
        if (q.startsWith("qid:") && StringUtils.isEmpty(requestParams.getWkt())
                && (requestParams.getFq().length == 0
                        || (requestParams.getFq().length == 1 && StringUtils.isEmpty(requestParams.getFq()[0])))) {
            double[] queryBBox = qidCacheDAO.get(q.substring(4)).getBbox();
            if (queryBBox != null && (queryBBox[0] > bbox[2] || queryBBox[2] < bbox[0] || queryBBox[1] > bbox[3]
                    || queryBBox[3] < bbox[1])) {
                displayBlankImage(response);
                return;
            }
        }

        String[] originalFqs = qidCacheDAO.getFq(requestParams);

        //get from cache, or make it
        boolean canCache = wmsCache.isEnabled() && cache.equalsIgnoreCase("on");
        WMSTile wco = getWMSCacheObject(requestParams, vars, pointType, bbox, originalFqs, boundingBoxFqs,
                canCache);

        //correction for gridDivisionCount
        boolean isGrid = vars.colourMode.equals("grid");
        if (isGrid) {
            if (gridDivisionCount > Math.min(width, height))
                gridDivisionCount = Math.min(width, height);
            if (gridDivisionCount < 0)
                gridDivisionCount = 1;

            //gridDivisionCount correction
            while (width % gridDivisionCount > 0 || height % gridDivisionCount > 0) {
                gridDivisionCount--;
            }
        }

        ImgObj imgObj = wco.getPoints() == null ? null
                : wmsCached(wco, requestParams, vars, pointType, pbbox, bbox, mbbox, width, height, width_mult,
                        height_mult, pointWidth, originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour,
                        response, is4326, tilebbox, gridDivisionCount);

        if (imgObj != null && imgObj.g != null) {
            imgObj.g.dispose();
            try {
                ServletOutputStream outStream = response.getOutputStream();
                ImageIO.write(imgObj.img, "png", outStream);
                outStream.flush();
                outStream.close();
            } catch (Exception e) {
                logger.debug("Unable to write image", e);
            }
        } else {
            displayBlankImage(response);
        }
    }

    /**
     * Method that produces the downloadable map integrated in AVH/OZCAM/Biocache.
     *
     * @param requestParams
     * @param format
     * @param extents
     * @param widthMm
     * @param pointRadiusMm
     * @param pradiusPx
     * @param pointColour
     * @param pointOpacity
     * @param baselayer
     * @param scale
     * @param dpi
     * @param outlinePoints
     * @param outlineColour
     * @param fileName
     * @param request
     * @param response
     * @throws Exception
     */
    @RequestMapping(value = { "/webportal/wms/image", "/mapping/wms/image" }, method = RequestMethod.GET)
    public void generatePublicationMap(SpatialSearchRequestParams requestParams,
            @RequestParam(value = "format", required = false, defaultValue = "jpg") String format,
            @RequestParam(value = "extents", required = true) String extents,
            @RequestParam(value = "widthmm", required = false, defaultValue = "60") Double widthMm,
            @RequestParam(value = "pradiusmm", required = false, defaultValue = "2") Double pointRadiusMm,
            @RequestParam(value = "pradiuspx", required = false) Integer pradiusPx,
            @RequestParam(value = "pcolour", required = false, defaultValue = "FF0000") String pointColour,
            @RequestParam(value = "popacity", required = false, defaultValue = "0.8") Double pointOpacity,
            @RequestParam(value = "baselayer", required = false, defaultValue = "world") String baselayer,
            @RequestParam(value = "scale", required = false, defaultValue = "off") String scale,
            @RequestParam(value = "dpi", required = false, defaultValue = "300") Integer dpi,
            @RequestParam(value = "outline", required = true, defaultValue = "false") boolean outlinePoints,
            @RequestParam(value = "outlineColour", required = true, defaultValue = "#000000") String outlineColour,
            @RequestParam(value = "fileName", required = false) String fileName,
            @RequestParam(value = "baseMap", required = false, defaultValue = "ALA") String baseMap,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        String[] bb = extents.split(",");

        double long1 = Double.parseDouble(bb[0]);
        double lat1 = Double.parseDouble(bb[1]);
        double long2 = Double.parseDouble(bb[2]);
        double lat2 = Double.parseDouble(bb[3]);

        if (lat1 <= -90) {
            lat1 = -89.999;
        }
        if (lat2 >= 90) {
            lat2 = 89.999;
        }

        int pminx = convertLngToPixel(long1);
        int pminy = convertLatToPixel(lat1);
        int pmaxx = convertLngToPixel(long2);
        int pmaxy = convertLatToPixel(lat2);

        int width = (int) ((dpi / 25.4) * widthMm);
        int height = (int) Math.round(width * ((pminy - pmaxy) / (double) (pmaxx - pminx)));

        if (height * width > MAX_IMAGE_PIXEL_COUNT) {
            String errorMessage = "Image size in pixels " + width + "x" + height + " exceeds "
                    + MAX_IMAGE_PIXEL_COUNT + " pixels.  Make the image smaller";
            response.sendError(response.SC_NOT_ACCEPTABLE, errorMessage);
            throw new Exception(errorMessage);
        }

        int pointSize = -1;
        if (pradiusPx != null) {
            pointSize = (int) pradiusPx;
        } else {
            pointSize = (int) ((dpi / 25.4) * pointRadiusMm);
        }

        double[] boundingBox = transformBbox4326To900913(Double.parseDouble(bb[0]), Double.parseDouble(bb[1]),
                Double.parseDouble(bb[2]), Double.parseDouble(bb[3]));

        //"http://biocache.ala.org.au/ws/webportal/wms/reflect?
        //q=macropus&ENV=color%3Aff0000%3Bname%3Acircle%3Bsize%3A3%3Bopacity%3A1
        //&BBOX=12523443.0512,-2504688.2032,15028131.5936,0.33920000120997&WIDTH=256&HEIGHT=256");
        String speciesAddress = baseWsUrl + "/ogc/wms/reflect?" + "ENV=color%3A" + pointColour
                + "%3Bname%3Acircle%3Bsize%3A" + pointSize + "%3Bopacity%3A" + pointOpacity + "&BBOX="
                + boundingBox[0] + "," + boundingBox[1] + "," + boundingBox[2] + "," + boundingBox[3] + "&WIDTH="
                + width + "&HEIGHT=" + height + "&OUTLINE=" + outlinePoints + "&OUTLINECOLOUR=" + outlineColour
                + "&" + request.getQueryString();

        URL speciesURL = new URL(speciesAddress);
        BufferedImage speciesImage = ImageIO.read(speciesURL);

        //"http://spatial.ala.org.au/geoserver/wms/reflect?
        //LAYERS=ALA%3Aworld&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=
        //&FORMAT=image%2Fjpeg&SRS=EPSG%3A900913&BBOX=12523443.0512,-1252343.932,13775787.3224,0.33920000004582&WIDTH=256&HEIGHT=256"
        String layout = "";
        if (!scale.equals("off")) {
            layout += "layout:scale";
        }
        String basemapAddress = geoserverUrl + "/wms/reflect?" + "LAYERS=ALA%3A" + baselayer
                + "&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=" + "&FORMAT=image%2Fpng&SRS=EPSG%3A900913" //specify the mercator projection
                + "&BBOX=" + boundingBox[0] + "," + boundingBox[1] + "," + boundingBox[2] + "," + boundingBox[3]
                + "&WIDTH=" + width + "&HEIGHT=" + height + "&OUTLINE=" + outlinePoints + "&format_options=dpi:"
                + dpi + ";" + layout;

        BufferedImage basemapImage;

        if ("roadmap".equalsIgnoreCase(baseMap) || "satellite".equalsIgnoreCase(baseMap)
                || "hybrid".equalsIgnoreCase(baseMap) || "terrain".equalsIgnoreCase(baseMap)) {
            basemapImage = basemapGoogle(width, height, boundingBox, baseMap);
        } else {
            basemapImage = ImageIO.read(new URL(basemapAddress));
        }

        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D combined = (Graphics2D) img.getGraphics();

        combined.drawImage(basemapImage, 0, 0, Color.WHITE, null);
        //combined.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pointOpacity.floatValue()));
        combined.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
        combined.drawImage(speciesImage, null, 0, 0);
        combined.dispose();

        //if filename supplied, force a download
        if (fileName != null) {
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Description", "File Transfer");
            response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
            response.setHeader("Content-Transfer-Encoding", "binary");
        } else if (format.equalsIgnoreCase("png")) {
            response.setContentType("image/png");
        } else {
            response.setContentType("image/jpeg");
        }

        try {
            if (format.equalsIgnoreCase("png")) {
                OutputStream os = response.getOutputStream();
                ImageIO.write(img, format, os);
                os.close();
            } else {
                //handle jpeg + BufferedImage.TYPE_INT_ARGB
                BufferedImage img2;
                Graphics2D c2;
                (c2 = (Graphics2D) (img2 = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB))
                        .getGraphics()).drawImage(img, 0, 0, Color.WHITE, null);
                c2.dispose();
                OutputStream os = response.getOutputStream();
                ImageIO.write(img2, format, os);
                os.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    private BufferedImage basemapGoogle(int width, int height, double[] extents, String maptype) throws Exception {

        double[] resolutions = { 156543.03390625, 78271.516953125, 39135.7584765625, 19567.87923828125,
                9783.939619140625, 4891.9698095703125, 2445.9849047851562, 1222.9924523925781, 611.4962261962891,
                305.74811309814453, 152.87405654907226, 76.43702827453613, 38.218514137268066, 19.109257068634033,
                9.554628534317017, 4.777314267158508, 2.388657133579254, 1.194328566789627, 0.5971642833948135 };

        //nearest resolution
        int imgSize = 640;
        int gScale = 2;
        double actualWidth = extents[2] - extents[0];
        double actualHeight = extents[3] - extents[1];
        int res = 0;
        while (res < resolutions.length - 1 && resolutions[res + 1] * imgSize > actualWidth
                && resolutions[res + 1] * imgSize > actualHeight) {
            res++;
        }

        int centerX = (int) ((extents[2] - extents[0]) / 2 + extents[0]);
        int centerY = (int) ((extents[3] - extents[1]) / 2 + extents[1]);
        double latitude = convertMetersToLat(centerY);
        double longitude = convertMetersToLng(centerX);

        //need to change the size requested so the extents match the output extents.
        int imgWidth = (int) ((extents[2] - extents[0]) / resolutions[res]);
        int imgHeight = (int) ((extents[3] - extents[1]) / resolutions[res]);

        String uri = "http://maps.googleapis.com/maps/api/staticmap?";
        String parameters = "center=" + latitude + "," + longitude + "&zoom=" + res + "&scale=" + gScale + "&size="
                + imgWidth + "x" + imgHeight + "&maptype=" + maptype;

        BufferedImage img = ImageIO.read(new URL(uri + parameters));

        BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        tmp.getGraphics().drawImage(img, 0, 0, width, height, 0, 0, imgWidth * gScale, imgHeight * gScale, null);

        return tmp;
    }

    /**
     * @return
     * @throws Exception
     */
    private ImgObj wmsCached(WMSTile wco, SpatialSearchRequestParams requestParams, WmsEnv vars,
            PointType pointType, double[] pbbox, double[] bbox, double[] mbbox, int width, int height,
            double width_mult, double height_mult, int pointWidth, String[] originalFqs, Set<Integer> hq,
            String[] boundingBoxFqs, boolean outlinePoints, String outlineColour, HttpServletResponse response,
            boolean is4326, double[] tilebbox, int gridDivisionCount) throws Exception {

        ImgObj imgObj = null;

        //grid setup
        boolean isGrid = vars.colourMode.equals("grid");
        int divs = gridDivisionCount; //number of x & y divisions in the WIDTH/H    EIGHT
        int[][] gridCounts = isGrid ? new int[divs][divs] : null;
        int xstep = width / divs;
        int ystep = height / divs;
        double grid_width_mult = (width / (pbbox[2] - pbbox[0])) / (width / divs);
        double grid_height_mult = (height / (pbbox[1] - pbbox[3])) / (height / divs);

        int x, y;

        //if not transparent and zero size, render dots
        if (vars.alpha > 0 && vars.size > 0) {
            List<float[]> points = wco.getPoints();
            List<int[]> counts = wco.getCounts();
            List<Integer> pColour = wco.getColours();
            if (pColour.size() == 1 && vars.colourMode.equals("-1")) {
                pColour.set(0, vars.colour | (vars.alpha << 24));
            }

            //initialise the image object
            imgObj = ImgObj.create(width, height);

            for (int j = 0; j < points.size(); j++) {

                if (hq != null && hq.contains(j)) {
                    //dont render these points
                    continue;
                }

                float[] ps = points.get(j);

                if (ps == null || ps.length == 0) {
                    continue;
                }

                //for 4326
                double top = tilebbox[3];
                double bottom = tilebbox[1];
                double left = tilebbox[0];
                double right = tilebbox[2];

                if (isGrid) {
                    //render grids
                    int[] count = counts.get(j);

                    //populate grid
                    for (int i = 0; i < ps.length; i += 2) {
                        float lng = ps[i];
                        float lat = ps[i + 1];
                        if (lng >= bbox[0] && lng <= bbox[2] && lat >= bbox[1] && lat <= bbox[3]) {
                            if (is4326) {
                                x = convertLngToPixel4326(lng, left, right, width);
                                y = convertLatToPixel4326(lat, top, bottom, height);
                            } else {
                                x = (int) ((convertLngToPixel(lng) - pbbox[0]) * grid_width_mult);
                                y = (int) ((convertLatToPixel(lat) - pbbox[3]) * grid_height_mult);
                            }

                            if (x >= 0 && x < divs && y >= 0 && y < divs) {
                                gridCounts[x][y] += count[i / 2];
                            }
                        }
                    }
                } else {
                    renderPoints(vars, bbox, pbbox, width_mult, height_mult, pointWidth, outlinePoints,
                            outlineColour, pColour, imgObj, j, ps, is4326, tilebbox, height, width);
                }
            }
        }

        //no points
        if (imgObj == null || imgObj.img == null) {
            if (vars.highlight == null) {
                displayBlankImage(response);
                return null;
            }
        } else if (isGrid) {
            //draw grid
            for (x = 0; x < divs; x++) {
                for (y = 0; y < divs; y++) {
                    int v = gridCounts[x][y];
                    if (v > 0) {
                        if (v > 500) {
                            v = 500;
                        }
                        int colour = (((500 - v) / 2) << 8) | (vars.alpha << 24) | 0x00FF0000;
                        imgObj.g.setColor(new Color(colour));
                        imgObj.g.fillRect(x * xstep, y * ystep, xstep, ystep);
                    }
                }
            }
        } else {
            drawUncertaintyCircles(requestParams, vars, height, width, pbbox, mbbox, bbox, width_mult, height_mult,
                    imgObj.g, originalFqs, boundingBoxFqs, is4326, tilebbox, pointType);
        }

        //highlight
        if (vars.highlight != null) {
            imgObj = drawHighlight(requestParams, vars, pointType, width, height, pbbox, width_mult, height_mult,
                    imgObj, originalFqs, boundingBoxFqs, is4326, tilebbox);
        }

        return imgObj;
    }

    void drawUncertaintyCircles(SpatialSearchRequestParams requestParams, WmsEnv vars, int height, int width,
            double[] pbbox, double[] mbbox, double[] bbox, double width_mult, double height_mult, Graphics2D g,
            String[] originalFqs, String[] boundingBoxFqs, boolean is4326, double[] tilebbox, PointType pointType)
            throws Exception {
        //draw uncertainty circles
        double hmult = (height / (mbbox[3] - mbbox[1]));

        //min uncertainty for current resolution and dot size
        double min_uncertainty = (vars.size + 1) / hmult;

        //only draw uncertainty if max radius will be > dot size
        if (vars.uncertainty && MAX_UNCERTAINTY > min_uncertainty) {

            //uncertainty colour/fq/radius, [0]=map, [1]=not specified, [2]=too large
            Color[] uncertaintyColours = { new Color(255, 170, 0, vars.alpha), new Color(255, 255, 100, vars.alpha),
                    new Color(50, 255, 50, vars.alpha) };
            //TODO: don't assume MAX_UNCERTAINTY > default_uncertainty
            String[] uncertaintyFqs = {
                    "coordinate_uncertainty:[" + min_uncertainty + " TO " + MAX_UNCERTAINTY
                            + "] AND -assertions:uncertaintyNotSpecified",
                    "assertions:uncertaintyNotSpecified", "coordinate_uncertainty:[" + MAX_UNCERTAINTY + " TO *]" };
            double[] uncertaintyR = { -1, MAX_UNCERTAINTY, MAX_UNCERTAINTY };

            String[] fqs = new String[originalFqs.length + 3];
            System.arraycopy(originalFqs, 0, fqs, 3, originalFqs.length);

            //expand bounding box to cover MAX_UNCERTAINTY radius (m to degrees)
            fqs[1] = "longitude:[" + (bbox[0] - MAX_UNCERTAINTY / 100000.0) + " TO "
                    + (bbox[2] + MAX_UNCERTAINTY / 100000.0) + "]";
            fqs[2] = "latitude:[" + (bbox[1] - MAX_UNCERTAINTY / 100000.0) + " TO "
                    + (bbox[3] + MAX_UNCERTAINTY / 100000.0) + "]";

            requestParams.setPageSize(DEFAULT_PAGE_SIZE);

            for (int j = 0; j < uncertaintyFqs.length; j++) {
                //do not display for [1]=not specified
                if (j == 1) {
                    continue;
                }

                fqs[0] = uncertaintyFqs[j];
                requestParams.setFq(fqs);

                //There can be performance issues with pivot when too many distinct coordinate_uncertainty values
                requestParams.setFacets(new String[] { "coordinate_uncertainty", pointType.getLabel() });
                requestParams.setFlimit(-1);
                requestParams.setFormattedQuery(null);
                List<FacetPivotResultDTO> qr = searchDAO.searchPivot(requestParams);

                if (qr != null && qr.size() > 0) {
                    List<FacetPivotResultDTO> piv = qr.get(0).getPivotResult();

                    //for 4326
                    double top = tilebbox[3];
                    double bottom = tilebbox[1];
                    double left = tilebbox[0];
                    double right = tilebbox[2];

                    double lng, lat;
                    int x, y;
                    int uncertaintyRadius = (int) Math.ceil(uncertaintyR[j] * hmult);

                    g.setColor(uncertaintyColours[j]);
                    for (FacetPivotResultDTO r : piv) {
                        if (uncertaintyR[j] < 0) {
                            uncertaintyRadius = (int) Math.ceil(Double.parseDouble(r.getValue()) * hmult);
                        }

                        for (FacetPivotResultDTO point : r.getPivotResult()) {
                            String[] lat_lng = point.getValue().split(",");

                            lng = Double.parseDouble(lat_lng[1]);
                            lat = Double.parseDouble(lat_lng[0]);

                            if (is4326) {
                                x = convertLngToPixel4326(lng, left, right, width);
                                y = convertLatToPixel4326(lat, top, bottom, height);
                            } else {
                                x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
                                y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
                            }

                            if (uncertaintyRadius > 0) {
                                g.drawOval(x - uncertaintyRadius, y - uncertaintyRadius, uncertaintyRadius * 2,
                                        uncertaintyRadius * 2);
                            } else {
                                g.drawRect(x, y, 1, 1);
                            }
                        }
                    }
                }
            }
        }
    }

    ImgObj drawHighlight(SpatialSearchRequestParams requestParams, WmsEnv vars, PointType pointType, int width,
            int height, double[] pbbox, double width_mult, double height_mult, ImgObj imgObj, String[] originalFqs,
            String[] boundingBoxFqs, boolean is4326, double[] tilebbox) throws Exception {
        String[] fqs = new String[originalFqs.length + 3];
        System.arraycopy(originalFqs, 0, fqs, 3, originalFqs.length);
        fqs[0] = vars.highlight;
        fqs[1] = boundingBoxFqs[0];
        fqs[2] = boundingBoxFqs[1];

        requestParams.setFq(fqs);
        requestParams.setFlimit(-1);
        requestParams.setFormattedQuery(null);
        FacetField ps = searchDAO.getFacetPointsShort(requestParams, pointType.getLabel());

        if (ps != null && ps.getValueCount() > 0) {
            if (imgObj == null || imgObj.img == null) { //when vars.alpha == 0 img is null
                imgObj = ImgObj.create(width, height);
            }

            int highightRadius = vars.size + HIGHLIGHT_RADIUS;
            int highlightWidth = highightRadius * 2;

            imgObj.g.setStroke(new BasicStroke(2));
            imgObj.g.setColor(new Color(255, 0, 0, 255));
            int x, y;

            //for 4326
            double top = tilebbox[3];
            double bottom = tilebbox[1];
            double left = tilebbox[0];
            double right = tilebbox[2];

            for (int i = 0; i < ps.getValueCount(); i++) {
                //extract lat lng
                if (ps.getValues().get(i).getName() != null) {
                    String[] lat_lng = ps.getValues().get(i).getName().split(",");
                    float lng = Float.parseFloat(lat_lng[1]);
                    float lat = Float.parseFloat(lat_lng[0]);

                    if (is4326) {
                        x = convertLngToPixel4326(lng, left, right, width);
                        y = convertLatToPixel4326(lat, top, bottom, height);
                    } else {
                        x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
                        y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
                    }

                    imgObj.g.drawOval(x - highightRadius, y - highightRadius, highlightWidth, highlightWidth);
                }
            }
        }

        return imgObj;
    }

    /**
     * Returns the wms cache object and initialises it if required.
     *
     * @param vars
     * @param pointType
     * @param requestParams
     * @param bbox
     * @return
     * @throws Exception
     */
    WMSTile getWMSCacheObject(SpatialSearchRequestParams requestParams, WmsEnv vars, PointType pointType,
            double[] bbox, String[] originalFqs, String[] boundingBoxFqs, boolean canCache) throws Exception {
        // do not cache this query if the cache is disabled or full
        if (wmsCache.isFull() || !wmsCache.isEnabled()) {
            canCache = false;
        }

        //caching is perTile
        String[] origAndBBoxFqs = null;
        if (originalFqs == null || originalFqs.length == 0) {
            origAndBBoxFqs = boundingBoxFqs;
        } else {
            origAndBBoxFqs = new String[originalFqs.length + 2];
            System.arraycopy(originalFqs, 0, origAndBBoxFqs, 2, originalFqs.length);
            origAndBBoxFqs[0] = boundingBoxFqs[0];
            origAndBBoxFqs[1] = boundingBoxFqs[1];
        }

        //replace qid with values for more cache hits
        String qparam = requestParams.getQ();
        if (qparam.startsWith("qid:")) {
            try {
                Qid qid = qidCacheDAO.get(qparam.substring(4));
                if (qid != null) {
                    qparam = qid.getQ() + qid.getWkt()
                            + (qid.getFqs() != null ? StringUtils.join(qid.getFqs(), ",") : "");
                }
            } catch (Exception e) {
            }
        }

        String qfull = qparam + StringUtils.join(requestParams.getFq(), ",") + requestParams.getQc()
                + requestParams.getWkt() + requestParams.getRadius() + requestParams.getLat()
                + requestParams.getLon();

        //qfull can be long if there is WKT 
        String q = String.valueOf(qfull.hashCode());

        //grid and -1 colour modes have the same data
        String cm = (vars.colourMode.equals("-1") || vars.colourMode.equals("grid")) ? "-1" : vars.colourMode;

        //if too many points, cache with bbox string
        boolean[] useBbox = new boolean[1];
        Integer count = 0;
        Integer pointsCount = 0;
        if (canCache) {
            //count docs
            count = getCachedCount(true, requestParams, q, pointType, useBbox);
            if (count == null || count == 0) {
                return new WMSTile();
            }

            //count unique points, if necessary
            if (count > wmsCacheMaxLayerPoints && pointType.getValue() > 0) {
                pointsCount = getCachedCount(false, requestParams, q, pointType, useBbox);

                //use bbox when too many points
                if (pointsCount != null && pointsCount > wmsCacheMaxLayerPoints) {
                    q += StringUtils.join(origAndBBoxFqs, ",");

                    requestParams.setFq(origAndBBoxFqs);
                    count = getCachedCount(true, requestParams, q, pointType, useBbox);
                    requestParams.setFq(originalFqs);

                    if (count == null || count == 0) {
                        return new WMSTile();
                    }

                    useBbox[0] = true;
                }
            }
        } else {
            queryFormatUtils.formatSearchQuery(requestParams, false);
        }

        List<LegendItem> colours = null;
        int sz = 0;
        WMSTile wco = null;
        if (canCache) {
            //iterate from lower value pointTypes up to this one
            for (int i = 0; !useBbox[0] && i < PointType.values().length && wco == null; i++) {
                if (PointType.values()[i].getValue() < pointType.getValue()) {
                    wco = wmsCache.getTest(q, cm, PointType.values()[i]);
                }
            }

            //not found, create it
            if (wco == null) {
                requestParams.setFlimit(-1);
                requestParams.setFormattedQuery(null);
                colours = cm.equals("-1") ? null : searchDAO.getColours(requestParams, vars.colourMode);
                sz = colours == null ? 1 : colours.size() + 1;

                wco = wmsCache.get(q, cm, pointType);
            }

            if (wco.getCached()) {
                return wco;
            }
        } else {
            wco = new WMSTile();
        }

        //still need colours when cannot cache
        requestParams.setFormattedQuery(null);
        if (colours == null && !cm.equals("-1"))
            colours = searchDAO.getColours(requestParams, vars.colourMode);

        //build only once
        synchronized (wco) {
            if (wco.getCached()) {
                return wco;
            }

            // when there is only one colour, return the result for colourMode=="-1" 
            if ((colours == null || colours.size() == 1) && !cm.equals("-1")) {
                String prevColourMode = vars.colourMode;
                vars.colourMode = "-1";
                WMSTile equivalentTile = getWMSCacheObject(requestParams, vars, pointType, bbox, originalFqs,
                        boundingBoxFqs, canCache);
                vars.colourMode = prevColourMode;

                //use the correct colour
                List<Integer> pColour = new ArrayList<Integer>(1);
                pColour.add(colours != null ? colours.get(0).getColour() | (vars.alpha << 24) : vars.colour);
                wco.setColours(pColour);

                wco.setBbox(bbox);
                wco.setColourmode(vars.colourMode);
                wco.setCounts(equivalentTile.getCounts());
                wco.setPoints(equivalentTile.getPoints());
                wco.setQuery(q);
            } else {
                //query with the bbox when it cannot be cached
                if (!canCache || useBbox[0])
                    requestParams.setFq(origAndBBoxFqs);

                List<Integer> pColour = new ArrayList<Integer>(sz);
                List<float[]> pointsArrays = new ArrayList<float[]>(sz);
                List<int[]> countsArrays = cm.equals("-1") ? new ArrayList<int[]>(sz) : null;

                queryTile(requestParams, vars, pointType, countsArrays, pointsArrays, colours, pColour, bbox,
                        originalFqs, boundingBoxFqs, canCache, count);

                wco.setBbox(bbox);
                wco.setColourmode(vars.colourMode);
                wco.setColours(pColour);
                if (cm.equals("-1"))
                    wco.setCounts(countsArrays);
                wco.setPoints(pointsArrays);
                wco.setQuery(q);
            }

            if (canCache) {
                wmsCache.put(q, cm, pointType, wco);
            }

            return wco;
        }
    }

    private Integer getCachedCount(boolean docCount, SpatialSearchRequestParams requestParams, String q,
            PointType pointType, boolean[] useBbox) throws Exception {

        Integer count = null;

        String tag = docCount ? "" : pointType.getLabel();

        synchronized (countLock) {
            count = (Integer) countsCache.get(q + tag);
        }
        if (count == null) {
            requestParams.setPageSize(0);
            requestParams.setFacet(true);
            requestParams.setFlimit(0);
            requestParams.setFacets(new String[] { pointType.getLabel() });
            requestParams.setFormattedQuery(null);
            if (docCount) {
                SolrDocumentList result = searchDAO.findByFulltext(requestParams);
                if (result != null) {
                    synchronized (countLock) {
                        count = (int) result.getNumFound();
                        countsCache.put(q + tag, count);
                    }
                }
            } else {
                List<GroupFacetResultDTO> result = searchDAO.searchGroupedFacets(requestParams);
                if (result != null && result.size() > 0) {
                    synchronized (countLock) {
                        count = result.get(0).getCount();
                        countsCache.put(q + tag, count);
                    }
                }
            }
        } else {
            queryFormatUtils.formatSearchQuery(requestParams, false);
        }

        return count;
    }

    private void queryTile(SpatialSearchRequestParams requestParams, WmsEnv vars, PointType pointType,
            List<int[]> countsArrays, List<float[]> pointsArrays, List<LegendItem> colours, List<Integer> pColour,
            double[] bbox, String[] originalFqs, String[] boundingBoxFqs, boolean canCache, int docCount)
            throws Exception {

        if (colours != null && colours.size() > 1) {
            long t1 = System.currentTimeMillis();
            String[] origFqs = requestParams.getFq();

            int colrmax = -1;
            long colrmaxtime = -1;

            boolean numericalFacetCategories = vars.colourMode.contains(",");

            //in some instances querying each colour's facet, one by one, is more suitable than pivoting
            if (numericalFacetCategories || docCount > wmsFacetPivotCutoff || !canCache) {
                //iterate
                String[] fqs = new String[requestParams.getFq() == null ? 1 : requestParams.getFq().length + 1];
                if (requestParams.getFq() != null && requestParams.getFq().length > 0) {
                    System.arraycopy(requestParams.getFq(), 0, fqs, 1, requestParams.getFq().length);
                }
                List<String> fqsDone = new ArrayList<String>(colours != null ? colours.size() : 0);

                //draw grouped points before drawing other points.
                boolean otherPointsAdded = false;
                pointsArrays.add(null);
                pColour.add(null);

                for (int i = 0; i < colours.size(); i++) {
                    LegendItem li = colours.get(i);

                    fqs[0] = li.getFq();

                    //invert fq
                    if (StringUtils.isEmpty(li.getName())) {
                        //li.getFq() is of the form "-(...)"
                        fqsDone.add(fqs[0].substring(1));
                    } else {
                        if (fqs[0].charAt(0) == '-') {
                            fqsDone.add(fqs[0].substring(1));
                        } else {
                            fqsDone.add("-" + fqs[0]);
                        }
                    }

                    requestParams.setFq(fqs);
                    long ms = System.currentTimeMillis();
                    requestParams.setFlimit(-1);

                    requestParams.setFormattedQuery(null);
                    makePointsFromFacet(searchDAO.getFacetPointsShort(requestParams, pointType.getLabel()),
                            pointsArrays, countsArrays);
                    pColour.add(li.getColour() | (vars.alpha << 24));
                    colrmaxtime = (System.currentTimeMillis() - ms);

                    //in the last iteration check for more and batch.
                    if (i == ColorUtil.colourList.length - 2 && colours.size() == ColorUtil.colourList.length - 1) {
                        colrmax = i;

                        fqs = new String[(requestParams.getFq() == null ? 0 : requestParams.getFq().length)
                                + fqsDone.size()];
                        if (requestParams.getFq() != null && requestParams.getFq().length > 0) {
                            System.arraycopy(requestParams.getFq(), 0, fqs, fqsDone.size(),
                                    requestParams.getFq().length);
                        }
                        System.arraycopy(fqsDone.toArray(new String[] {}), 0, fqs, 0, fqsDone.size());

                        //do full query
                        requestParams.setFq(origFqs);
                        String prevColourMode = vars.colourMode;
                        vars.colourMode = "-1";
                        WMSTile equivalentTile = getWMSCacheObject(requestParams, vars, pointType, bbox,
                                originalFqs, boundingBoxFqs, canCache);
                        vars.colourMode = prevColourMode;

                        if (equivalentTile.getPoints() != null && equivalentTile.getPoints().size() > 0
                                && equivalentTile.getPoints().get(0).length > 0) {
                            pointsArrays.set(0, equivalentTile.getPoints().get(0));
                            //countsArrays.add(equivalentTile.getCounts().get(0));
                            pColour.set(0,
                                    ColorUtil.colourList[ColorUtil.colourList.length - 1] | (vars.alpha << 24));
                            otherPointsAdded = true;
                        }
                    }
                }

                if (!otherPointsAdded) {
                    pointsArrays.remove(0);
                    pColour.remove(0);
                }

                //restore the altered fqs
                requestParams.setFq(origFqs);
            }

            long t2 = System.currentTimeMillis();

            //pivot
            if (!numericalFacetCategories && docCount <= wmsFacetPivotCutoff && canCache) {
                requestParams.setFacets(new String[] { vars.colourMode + "," + pointType.getLabel() });
                requestParams.setFlimit(-1);
                requestParams.setFormattedQuery(null);
                //get pivot and drill to colourMode level
                List<FacetPivotResultDTO> qr = searchDAO.searchPivot(requestParams);
                if (qr != null && qr.size() > 0) {
                    List<FacetPivotResultDTO> piv = qr.get(0).getPivotResult();

                    //last colour
                    int lastColour = ColorUtil.colourList[ColorUtil.colourList.length - 1] | (vars.alpha << 24);

                    //get facet points, retain colours order so it does not break hq
                    for (int i = 0; i < colours.size(); i++) {
                        LegendItem li = colours.get(i);

                        int j = 0;
                        while (j < piv.size() && piv.size() > 0) {
                            FacetPivotResultDTO p = piv.get(j);
                            if ((StringUtils.isEmpty(li.getName()) && StringUtils.isEmpty(p.getValue()))
                                    || (StringUtils.isNotEmpty(li.getName())
                                            && li.getName().equals(p.getValue()))) {
                                makePointsFromPivot(p.getPivotResult(), pointsArrays, countsArrays);
                                pColour.add(li.getColour() | (vars.alpha << 24));
                                piv.remove(j);
                                break;
                            } else {
                                j++;
                            }
                        }
                    }

                    // ensure all point/colour pairs are added
                    while (piv.size() > 0) {
                        makePointsFromPivot(piv.get(0).getPivotResult(), pointsArrays, countsArrays);
                        pColour.add(lastColour);
                        piv.remove(0);
                    }
                }
            }

            long t3 = System.currentTimeMillis();

            if (logger.isDebugEnabled()) {
                int occurrences = 0;
                int points = 0;
                int colourMatches = pointsArrays.size();
                for (int i = 0; i < pointsArrays.size(); i++) {
                    points += pointsArrays.get(i).length;
                    if (countsArrays != null) {
                        for (int j = 0; j < pointsArrays.get(i).length; j++) {
                            occurrences += countsArrays.get(i)[j];
                        }
                    }
                }

                logger.debug("wms timings: many queries=" + (t2 - t1) + "ms, pivot=" + (t3 - t2) + "ms, "
                        + "colours=" + colours.size() + ", points=" + points + ", occurrences=" + occurrences + ", "
                        + "matchedColours=" + colourMatches + ", max colour idx=" + colrmax + ", "
                        + "max colour request time=" + colrmaxtime + "ms, query docCount=" + docCount);
            }

        }
        //get points for occurrences not in colours.
        if (colours == null || colours.isEmpty() || colours.size() == 1) {
            requestParams.setFlimit(-1);
            requestParams.setFormattedQuery(null);
            makePointsFromFacet(searchDAO.getFacetPointsShort(requestParams, pointType.getLabel()), pointsArrays,
                    countsArrays);
            if (colours == null || colours.isEmpty()) {
                pColour.add(vars.colour);
            } else {
                pColour.add(colours.get(0).getColour() | (vars.alpha << 24));
            }
        }
    }

    private void makePointsFromPivot(List<FacetPivotResultDTO> pivotResult, List gPoints, List gCount) {
        float[] points = new float[2 * pivotResult.size()];
        int[] count = new int[pivotResult.size()];
        int i = 0;
        int j = 0;
        for (FacetPivotResultDTO fpr : pivotResult) {
            String v = fpr.getValue();
            if (StringUtils.isNotEmpty(v)) {
                int p = v.indexOf(',');
                points[i++] = Float.parseFloat(v.substring(p + 1));
                points[i++] = Float.parseFloat(v.substring(0, p));
                count[j++] = fpr.getCount();
            } else {
                points[i++] = Float.NaN;
                i++;
                count[j++] = 0;
            }
        }

        gPoints.add(points);
        if (gCount != null)
            gCount.add(count);
    }

    private void makePointsFromFacet(FacetField facet, List gPoints, List gCount) {
        float[] points = new float[2 * facet.getValues().size()];
        int[] count = new int[facet.getValues().size()];
        int i = 0;
        int j = 0;
        for (FacetField.Count s : facet.getValues()) {
            try {
                String v = s.getName();
                if (v != null) {
                    int p = v.indexOf(',');
                    float lng = Float.parseFloat(v.substring(p + 1));
                    float lat = Float.parseFloat(v.substring(0, p));
                    int c = (int) s.getCount();
                    points[i++] = lng;
                    points[i++] = lat;
                    count[j++] = c;
                } else {
                    points[i++] = Float.NaN;
                    i++;
                    count[j++] = 0;
                }
            } catch (Exception e) {
            }
        }

        //duplicate the last point in case of errors above
        while (i < points.length && i > 1) {
            points[i] = points[i - 2];
            i++;
        }
        while (j < count.length && j > 0) {
            count[j] = 0;
            j++;
        }

        gPoints.add(points);
        if (gCount != null)
            gCount.add(count);
    }

    private void renderPoints(WmsEnv vars, double[] bbox, double[] pbbox, double width_mult, double height_mult,
            int pointWidth, boolean outlinePoints, String outlineColour, List<Integer> pColour, ImgObj imgObj,
            int j, float[] ps, boolean is4326, double[] tilebbox, int height, int width) {
        int x;
        int y;
        Paint currentFill = new Color(pColour.get(j), true);
        imgObj.g.setPaint(currentFill);
        Color oColour = Color.decode(outlineColour);

        //for 4326
        double top = tilebbox[3];
        double bottom = tilebbox[1];
        double left = tilebbox[0];
        double right = tilebbox[2];

        for (int i = 0; i < ps.length; i += 2) {
            float lng = ps[i];
            float lat = ps[i + 1];

            if (lng >= bbox[0] && lng <= bbox[2] && lat >= bbox[1] && lat <= bbox[3]) {

                if (is4326) {
                    x = convertLngToPixel4326(lng, left, right, width);
                    y = convertLatToPixel4326(lat, top, bottom, height);
                } else {
                    x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
                    y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
                }

                //System.out.println("Drawing an oval.....");
                imgObj.g.fillOval(x - vars.size, y - vars.size, pointWidth, pointWidth);
                if (outlinePoints) {
                    imgObj.g.setPaint(oColour);
                    imgObj.g.drawOval(x - vars.size, y - vars.size, pointWidth, pointWidth);
                    imgObj.g.setPaint(currentFill);
                }
            }
        }
    }

    //method from 1.3.3.1 Mercator (Spherical) http://www.epsg.org/guides/docs/g7-2.pdf
    //constant from EPSG:900913
    private double[] transformBbox4326To900913(double long1, double lat1, double long2, double lat2) {
        return new double[] { 6378137.0 * long1 * Math.PI / 180.0,
                6378137.0 * Math.log(Math.tan(Math.PI / 4.0 + lat1 * Math.PI / 360.0)),
                6378137.0 * long2 * Math.PI / 180.0,
                6378137.0 * Math.log(Math.tan(Math.PI / 4.0 + lat2 * Math.PI / 360.0)) };
    }

    public void setTaxonDAO(TaxonDAO taxonDAO) {
        this.taxonDAO = taxonDAO;
    }

    public void setSearchDAO(SearchDAO searchDAO) {
        this.searchDAO = searchDAO;
    }

    public void setSearchUtils(SearchUtils searchUtils) {
        this.searchUtils = searchUtils;
    }

    public void setBaseWsUrl(String baseWsUrl) {
        this.baseWsUrl = baseWsUrl;
    }

    public void setOrganizationName(String organizationName) {
        this.organizationName = organizationName;
    }

    public void setOrgCity(String orgCity) {
        this.orgCity = orgCity;
    }

    public void setOrgStateProvince(String orgStateProvince) {
        this.orgStateProvince = orgStateProvince;
    }

    public void setOrgPostcode(String orgPostcode) {
        this.orgPostcode = orgPostcode;
    }

    public void setOrgCountry(String orgCountry) {
        this.orgCountry = orgCountry;
    }

    public void setOrgPhone(String orgPhone) {
        this.orgPhone = orgPhone;
    }

    public void setOrgFax(String orgFax) {
        this.orgFax = orgFax;
    }

    public void setOrgEmail(String orgEmail) {
        this.orgEmail = orgEmail;
    }
}