mbohun
7/9/2014 - 2:59 AM

Fix for 4326 in WMS, Scatterplot service, Fix for fqs in qids in WMS

Fix for 4326 in WMS, Scatterplot service, Fix for fqs in qids in WMS

# checkout the svn project
bash-3.2$ cd ~/src
bash-3.2$ svn checkout http://ala-portal.googlecode.com/svn/trunk/biocache-service svn_biocache-service
bash-3.2$ cd svn_biocache-service

# generate the patch
bash-3.2$ svn diff -c r4552 ./ > Fix_for_4326_in_WMS-Scatterplot_service.patch

# OR use: svn diff -r $(SOME_REVISION):$(SOME_REVISION+n) ./ > your_patch_name.patch
bash-3.2$ svn diff -r r4535:r4552 ./ > Fix_for_4326_in_WMS-Scatterplot_service-revisions-diff.patch

# checkout the git version of the project
bash-3.2$ cd ~/src
bash-3.2$ git clone git@github.com:AtlasOfLivingAustralia/biocache-service.git
bash-3.2$ cd biocache-service

# review/test the patch/changes from svn with --dry-run
bash-3.2$ patch -p0 < ../svn_biocache-service/Fix_for_4326_in_WMS-Scatterplot_service.patch --dry-run

# apply the patch/changes from svn to the git version
bash-3.2$ patch -p0 < ../svn_biocache-service/Fix_for_4326_in_WMS-Scatterplot_service.patch
patching file src/main/java/au/org/ala/biocache/web/ScatterplotController.java
patching file src/main/java/au/org/ala/biocache/web/WMSController.java
patching file pom.xml

# verify what changed
bash-3.2$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   pom.xml
        modified:   src/main/java/au/org/ala/biocache/web/WMSController.java

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        src/main/java/au/org/ala/biocache/web/ScatterplotController.java

no changes added to commit (use "git add" and/or "git commit -a")

# add the new files, etc.
bash-3.2$ git add src/main/java/au/org/ala/biocache/web/ScatterplotController.java
bash-3.2$ git add pom.xml
bash-3.2$ git add src/main/java/au/org/ala/biocache/web/WMSController.java

# review
bash-3.2$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   pom.xml
        new file:   src/main/java/au/org/ala/biocache/web/ScatterplotController.java
        modified:   src/main/java/au/org/ala/biocache/web/WMSController.java

bash-3.2$ git commit -m "Fix for 4326 in WMS, Scatterplot service, Fix for fqs in qids in WMS"
[master 41722dd] Fix for 4326 in WMS, Scatterplot service, Fix for fqs in qids in WMS
 3 files changed, 474 insertions(+), 41 deletions(-)
 create mode 100644 src/main/java/au/org/ala/biocache/web/ScatterplotController.java

# push, send pull request, etc.
bash-3.2$ git push

###

result: https://github.com/AtlasOfLivingAustralia/biocache-service/commit/41722dd707bc8d985e7f0aecbcb9c2b8a6dcfc91

Index: src/main/java/au/org/ala/biocache/web/ScatterplotController.java
===================================================================
--- src/main/java/au/org/ala/biocache/web/ScatterplotController.java	(revision 0)
+++ src/main/java/au/org/ala/biocache/web/ScatterplotController.java	(revision 4552)
@@ -0,0 +1,230 @@
+package au.org.ala.biocache.web;
+
+import au.org.ala.biocache.dao.SearchDAO;
+import au.org.ala.biocache.dto.IndexFieldDTO;
+import au.org.ala.biocache.dto.SearchResultDTO;
+import au.org.ala.biocache.dto.SpatialSearchRequestParams;
+import org.apache.log4j.Logger;
+import org.apache.solr.common.SolrDocumentList;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartRenderingInfo;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.encoders.EncoderUtil;
+import org.jfree.chart.encoders.ImageFormat;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.data.xy.DefaultXYDataset;
+import org.jfree.ui.RectangleEdge;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletResponse;
+import java.awt.*;
+import java.awt.geom.Ellipse2D;
+import java.awt.image.BufferedImage;
+import java.util.*;
+
+/**
+ * This controller is responsible for providing basic scatterplot services.
+ *
+ * Basic scatterplot is
+ * - occurrences, standard biocache query
+ * - x, numerical stored value field
+ * - y, numerical stored value field
+ * - height, integer default 256
+ * - width, integer default 256
+ * - title, string default query-display-name
+ * - pointcolour, colour as RGB string like FF0000 for red, default 0000FF
+ * - pointradius, double default 3
+ *
+ */
+@Controller
+public class ScatterplotController {
+
+    private static Logger logger = Logger.getLogger(ScatterplotController.class);
+
+    private final static int PAGE_SIZE = 100000000;
+    private final static String DEFAULT_SCATTERPLOT_TITLE = " ";
+    private final static String DEFAULT_SCATTERPLOT_HEIGHT = "256";
+    private final static String DEFAULT_SCATTERPLOT_WIDTH = "256";
+    private final static String DEFAULT_SCATTERPLOT_POINTCOLOUR = "0000FF";
+    private final static String DEFAULT_SCATTERPLOT_POINTRADIUS = "3";
+    private final static String [] VALID_DATATYPES = {"double","int","long"};
+
+    @Inject
+    protected SearchDAO searchDAO;
+
+    @RequestMapping(value = {"/scatterplot"}, method = RequestMethod.GET)
+    public void scatterplot(SpatialSearchRequestParams requestParams,
+                            @RequestParam(value = "x", required = true) String x,
+                            @RequestParam(value = "y", required = true) String y,
+                            @RequestParam(value = "height", required = false, defaultValue=DEFAULT_SCATTERPLOT_HEIGHT) Integer height,
+                            @RequestParam(value = "width", required = false, defaultValue=DEFAULT_SCATTERPLOT_WIDTH) Integer width,
+                            @RequestParam(value = "title", required = false, defaultValue=DEFAULT_SCATTERPLOT_TITLE) String title,
+                            @RequestParam(value = "pointcolour", required = false, defaultValue=DEFAULT_SCATTERPLOT_POINTCOLOUR) String pointcolour,
+                            @RequestParam(value = "pointradius", required = false, defaultValue = DEFAULT_SCATTERPLOT_POINTRADIUS) Double pointradius,
+                            HttpServletResponse response) throws Exception {
+        JFreeChart jChart = makeScatterplot(requestParams, x, y, title, pointcolour, pointradius);
+
+        //produce image
+        ChartRenderingInfo chartRenderingInfo = new ChartRenderingInfo();
+        BufferedImage bi = jChart.createBufferedImage(width, height, BufferedImage.TRANSLUCENT, chartRenderingInfo);
+        byte[] bytes = EncoderUtil.encode(bi, ImageFormat.PNG, true);
+
+        //output image
+        response.setContentType("image/png");
+        response.getOutputStream().write(bytes);
+    }
+
+    @RequestMapping(value = {"/scatterplot/point"}, method = RequestMethod.GET)
+    public Map scatterplotPointInfo(SpatialSearchRequestParams requestParams,
+                            @RequestParam(value = "x", required = true) String x,
+                            @RequestParam(value = "y", required = true) String y,
+                            @RequestParam(value = "height", required = false, defaultValue=DEFAULT_SCATTERPLOT_HEIGHT) Integer height,
+                            @RequestParam(value = "width", required = false, defaultValue=DEFAULT_SCATTERPLOT_WIDTH) Integer width,
+                            @RequestParam(value = "title", required = false, defaultValue=DEFAULT_SCATTERPLOT_TITLE) String title,
+                            @RequestParam(value = "pointx1", required = true) Integer pointx1,
+                            @RequestParam(value = "pointy1", required = true) Integer pointy1,
+                            @RequestParam(value = "pointx2", required = true) Integer pointx2,
+                            @RequestParam(value = "pointy2", required = true) Integer pointy2) throws Exception {
+
+        JFreeChart jChart = makeScatterplot(requestParams, x, y, title, "000000", 1.0);
+
+        //produce image
+        ChartRenderingInfo chartRenderingInfo = new ChartRenderingInfo();
+        BufferedImage bi = jChart.createBufferedImage(width, height, BufferedImage.TRANSLUCENT, chartRenderingInfo);
+
+        XYPlot plot = (XYPlot) jChart.getPlot();
+
+        //identify point range across x and y
+        double tx1 = plot.getRangeAxis().java2DToValue(pointx1, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.BOTTOM);
+        double tx2 = plot.getRangeAxis().java2DToValue(pointx2, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.BOTTOM);
+        double ty1 = plot.getDomainAxis().java2DToValue(pointy1, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.LEFT);
+        double ty2 = plot.getDomainAxis().java2DToValue(pointy2, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.LEFT);
+        double x1 = Math.min(tx1, tx2);
+        double x2 = Math.max(tx1, tx2);
+        double y1 = Math.min(ty1, ty2);
+        double y2 = Math.max(ty1, ty2);
+
+        Map map = new HashMap();
+        map.put("xaxis_pixel_selection",new int[] {pointx1, pointx2});
+        map.put("yaxis_pixel_selection",new int[] {pointy1, pointy2});
+        map.put("xaxis",x);
+        map.put("yaxis",y);
+        map.put("xaxis_range",new double[]{x1, x2});
+        map.put("yaxis_range",new double[]{y1, y2});
+
+        return map;
+
+        /*
+        //add new fqs
+        String [] fqs_old = requestParams.getFq();
+        String [] fqs_new = new String[fqs_old.length + 2];
+        System.arraycopy(fqs_old,0,fqs_new,0,fqs_old.length);
+        fqs_new[fqs_old.length] = x + ":[" + x1 + " TO " + x2 + "]";
+        fqs_new[fqs_old.length + 1] = y + ":[" + y1 + " TO " + y2 + "]";
+        requestParams.setFq(fqs_new);
+        return searchDAO.findByFulltextSpatialQuery(requestParams, null);
+        */
+    }
+
+    JFreeChart makeScatterplot(SpatialSearchRequestParams requestParams, String x, String y
+        , String title, String pointcolour, Double pointradius) throws Exception {
+        //verify x and y are numerical and stored
+        String displayNameX = null;
+        String displayNameY = null;
+        for (IndexFieldDTO indexFieldDTO : searchDAO.getIndexFieldDetails(new String[]{x}) ){
+            if (!Arrays.asList(VALID_DATATYPES).contains(indexFieldDTO.getDataType() )) {
+                throw new Exception("Invalid datatype: " + indexFieldDTO.getDataType() + " for x: " + x);
+            }
+            if(!indexFieldDTO.isStored()) {
+                throw new Exception("Cannot use x: " + x + ".  It is not a stored field.");
+            }
+            displayNameX = indexFieldDTO.getDescription();
+        }
+        for (IndexFieldDTO indexFieldDTO : searchDAO.getIndexFieldDetails(new String[]{y}) ){
+            if (!Arrays.asList(VALID_DATATYPES).contains(indexFieldDTO.getDataType() )) {
+                throw new Exception("Invalid datatype: " + indexFieldDTO.getDataType() + " for y: " + y);
+            }
+            if(!indexFieldDTO.isStored()) {
+                throw new Exception("Cannot use y: " + y + ".  It is not a stored field.");
+            }
+            displayNameY = indexFieldDTO.getDescription();
+        }
+        if(displayNameX == null) {
+            throw new Exception("Unknown value for x: " + x);
+        }
+        if(displayNameY == null) {
+            throw new Exception("Unknown value for y: " + y);
+        }
+
+        //get data
+        requestParams.setPageSize(PAGE_SIZE);
+        requestParams.setFl(x + "," + y);
+        SolrDocumentList sdl = searchDAO.findByFulltext(requestParams);
+        int size = sdl.size();
+        double [][] data = new double[2][size];
+        int count = 0;
+        for(int i=0;i<size;i++) {
+            try {
+                Object a = sdl.get(i).getFieldValue(y);
+                Object b = sdl.get(i).getFieldValue(x);
+                data[1][i] = Double.parseDouble(String.valueOf(sdl.get(i).getFieldValue(y)));
+                if(a instanceof Double) {
+                    data[0][i] = (Double) a;
+                } else {
+                    data[0][i] = Double.parseDouble(String.valueOf(a));
+                }
+
+                if(b instanceof Double) {
+                    data[1][i] = (Double) b;
+                } else {
+                    data[1][i] = Double.parseDouble(String.valueOf(b));
+                }
+
+                count++;
+            } catch (Exception e) {
+                data[0][i] = Double.NaN;
+                data[1][i] = Double.NaN;
+            }
+        }
+
+        if(count == 0) {
+            throw new Exception("valid records found for these input parameters");
+        }
+
+        //create dataset
+        DefaultXYDataset xyDataset = new DefaultXYDataset();
+        xyDataset.addSeries("series", data);
+
+        //create chart
+        JFreeChart jChart = ChartFactory.createScatterPlot(
+                title.equals(" ")?requestParams.getDisplayString():title //chart display name
+                , displayNameX //x-axis display name
+                , displayNameY //y-axis display name
+                , xyDataset
+                , PlotOrientation.HORIZONTAL, false, false, false);
+        jChart.setBackgroundPaint(Color.white);
+
+        //styling
+        XYPlot plot = (XYPlot) jChart.getPlot();
+        Font axisfont = new Font("Arial", Font.PLAIN, 10);
+        Font titlefont = new Font("Arial", Font.BOLD, 11);
+        plot.getDomainAxis().setLabelFont(axisfont);
+        plot.getDomainAxis().setTickLabelFont(axisfont);
+        plot.getRangeAxis().setLabelFont(axisfont);
+        plot.getRangeAxis().setTickLabelFont(axisfont);
+        plot.setBackgroundPaint(new Color(220, 220, 220));
+        jChart.getTitle().setFont(titlefont);
+
+        //point shape and colour
+        Color c = new Color(Integer.parseInt(pointcolour, 16));
+        plot.getRenderer().setSeriesPaint(0, c);
+        plot.getRenderer().setSeriesShape(0, new Ellipse2D.Double(-pointradius, -pointradius, pointradius*2, pointradius*2));
+
+        return jChart;
+    }
+}
Index: src/main/java/au/org/ala/biocache/web/WMSController.java
===================================================================
--- src/main/java/au/org/ala/biocache/web/WMSController.java	(revision 4551)
+++ src/main/java/au/org/ala/biocache/web/WMSController.java	(revision 4552)
@@ -152,7 +152,7 @@
         //store the title if necessary
         if(title == null)
             title = requestParams.getDisplayString();
-        String[] fqs = requestParams.getFq();
+        String[] fqs = getFq(requestParams);
         if(fqs != null && fqs.length==1 && fqs[0].length()==0){
             fqs =null;
         }
@@ -537,6 +537,14 @@
                 / (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);
     }
@@ -606,11 +614,12 @@
      * @param pbbox  the pbbox to initialise
      * @return
      */
-    private double getBBoxes(String bboxString, int width, int height, int size, boolean uncertainty, double[] mbbox, double[] bbox, double[] pbbox) {
+    private double getBBoxes(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 {
-                mbbox[i] = Double.parseDouble(s);
+                tilebbox[i] = Double.parseDouble(s);
+                mbbox[i] = tilebbox[i];
                 i++;
             } catch (Exception e) {
                 logger.error("Problem parsing BBOX: '" + bboxString + "'", e);
@@ -658,6 +667,75 @@
         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;
+
+        //check offset for points bb by maximum uncertainty
+        if (uncertainty) {
+            //estimate 0.01 degrees is 1000m
+            double scale = 0.01 / 1000;
+            if (xoffset < MAX_UNCERTAINTY * scale) {
+                xoffset = MAX_UNCERTAINTY * scale;
+            }
+            if (yoffset < MAX_UNCERTAINTY * scale) {
+                yoffset = MAX_UNCERTAINTY * scale;
+            }
+        }
+
+        //adjust offset for pixel height/width
+        xoffset += pixelWidth;
+        yoffset += pixelHeight;
+
+        /* not required for 4326
+        pbbox[0] = convertLngToPixel(convertMetersToLng(mbbox[0]));
+        pbbox[1] = convertLatToPixel(convertMetersToLat(mbbox[1]));
+        pbbox[2] = convertLngToPixel(convertMetersToLng(mbbox[2]));
+        pbbox[3] = convertLatToPixel(convertMetersToLat(mbbox[3]));
+        */
+
+        //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:");
@@ -697,7 +775,7 @@
             SpatialSearchRequestParams requestParams = new SpatialSearchRequestParams();
             requestParams.setQ(request.getQ());
             requestParams.setQc(request.getQc());
-            requestParams.setFq(request.getFq());
+            requestParams.setFq(getFq(request));
 
             //test for cutpoints on the back of colourMode
             String[] s = colourMode.split(",");
@@ -785,7 +863,7 @@
         String[] dir = {"asc", "asc", "desc", "desc"};
 
         //remove instances of null longitude or latitude
-        String[] fq = (String[]) ArrayUtils.addAll(requestParams.getFq(), new String[]{"longitude:[* TO *]", "latitude:[* TO *]"});
+        String[] fq = (String[]) ArrayUtils.addAll(getFq(requestParams), new String[]{"longitude:[* TO *]", "latitude:[* TO *]"});
         requestParams.setFq(fq);
         requestParams.setPageSize(10);
 
@@ -959,10 +1037,11 @@
         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);
+        double resolution = getBBoxes(bboxString, width, height, size, vars.uncertainty, mbbox, bbox, pbbox, tilebbox);
 
         //resolution should be a value < 1
         PointType pointType = getPointTypeForDegreesPerPixel(resolution);
@@ -1401,16 +1480,24 @@
         response.setHeader("Cache-Control", "max-age=86400"); //age == 1 day
         response.setContentType("image/png"); //only png images generated
 
-        if("EPSG:4326".equals(srs))
-            bboxString = convertBBox4326To900913(bboxString);    // to work around a UDIG bug
-
+        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];
         int size = vars.size + (vars.highlight != null ? HIGHLIGHT_RADIUS * 2 + (int) (vars.size * 0.2) : 0) + 5;  //bounding box buffer
 
-        double resolution = getBBoxes(bboxString, width, height, size, vars.uncertainty, mbbox, bbox, pbbox);
+        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());
 
@@ -1421,7 +1508,7 @@
             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]);
@@ -1434,6 +1521,8 @@
         //build request
         if (q.length() > 0) {
             requestParams.setQ(q);
+        } else {
+            q = requestParams.getQ();
         }
 
         //bounding box test (q must be 'qid:' + number)
@@ -1446,7 +1535,7 @@
             }
         }
 
-        String[] originalFqs = requestParams.getFq();
+        String[] originalFqs = getFq(requestParams);
 
         //get from cache
         WMSTile wco = null;
@@ -1460,11 +1549,11 @@
         if (wco == null) {
             imgObj = wmsUncached(requestParams, vars, pointType, pbbox, mbbox,
                     width, height, width_mult, height_mult, pointWidth,
-                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response);
+                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response, is4326, tilebbox);
         } else {
             imgObj = wmsCached(wco, requestParams, vars, pointType, pbbox, bbox, mbbox,
                     width, height, width_mult, height_mult, pointWidth,
-                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response);
+                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response, is4326, tilebbox);
         }
 
         if (imgObj != null && imgObj.g != null) {
@@ -1638,7 +1727,8 @@
                              double height_mult, int pointWidth, String[] originalFqs, Set<Integer> hq,
                              String[] boundingBoxFqs, boolean outlinePoints,
                              String outlineColour,
-                             HttpServletResponse response) throws Exception {
+                             HttpServletResponse response,
+                             boolean is4326, double [] tilebbox) throws Exception {
 
         ImgObj imgObj = null;
 
@@ -1677,6 +1767,12 @@
                     continue;
                 }
 
+                //for 4326
+                double top = tilebbox[3];
+                double bottom = tilebbox[1];
+                double left = tilebbox[0];
+                double right = tilebbox[2];
+
                 if (vars.colourMode.equals("grid")) {
                     //render grids
                     int[] count = counts.get(j);
@@ -1687,9 +1783,16 @@
                         float lat = ps[i + 1];
                         if (lng >= bbox[0] && lng <= bbox[2]
                                 && lat >= bbox[1] && lat <= bbox[3]) {
-                            x = (int) ((convertLngToPixel(lng) - pbbox[0]) * grid_width_mult);
-                            y = (int) ((convertLatToPixel(lat) - pbbox[3]) * grid_height_mult);
 
+
+                            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];
                             }
@@ -1705,9 +1808,15 @@
                         float lat = ps[i + 1];
                         if (lng >= bbox[0] && lng <= bbox[2]
                                 && lat >= bbox[1] && lat <= bbox[3]) {
-                            x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-                            y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
 
+                            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.fillOval(x - vars.size, y - vars.size, pointWidth, pointWidth);
                             if(outlinePoints){
                                 imgObj.g.setPaint(oColour);
@@ -1742,18 +1851,18 @@
                 }
             }
         } else {
-            drawUncertaintyCircles(requestParams, vars, height, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs);
+            drawUncertaintyCircles(requestParams, vars, height, width, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs, is4326, tilebbox);
         }
 
         //highlight
         if (vars.highlight != null) {
-            imgObj = drawHighlight(requestParams, vars, pointType, width, height, pbbox, width_mult, height_mult, imgObj, originalFqs, boundingBoxFqs);
+            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, double[] pbbox, double[] mbbox, double width_mult, double height_mult, Graphics2D g, String[] originalFqs, String[] boundingBoxFqs) throws Exception {
+    void drawUncertaintyCircles(SpatialSearchRequestParams requestParams, WmsEnv vars, int height, int width, double[] pbbox, double[] mbbox, double width_mult, double height_mult, Graphics2D g, String[] originalFqs, String[] boundingBoxFqs, boolean is4326, double [] tilebbox) throws Exception {
         //draw uncertainty circles
         double hmult = (height / (mbbox[3] - mbbox[1]));
 
@@ -1787,6 +1896,12 @@
                 //TODO: paging
                 SolrDocumentList sdl = searchDAO.findByFulltext(requestParams);
 
+                //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);
@@ -1800,8 +1915,13 @@
                         lng = (Double) sdl.get(i).getFieldValue("longitude");
                         lat = (Double) sdl.get(i).getFieldValue("latitude");
 
-                        x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-                        y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
+                        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);
@@ -1816,7 +1936,7 @@
 
     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) throws Exception {
+                         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;
@@ -1837,13 +1957,25 @@
             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.size(); i++) {
                 OccurrencePoint pt = ps.get(i);
                 float lng = pt.getCoordinates().get(0).floatValue();
                 float lat = pt.getCoordinates().get(1).floatValue();
 
-                x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-                y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
+                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);
             }
@@ -1888,7 +2020,7 @@
             //points count
             SpatialSearchRequestParams r = new SpatialSearchRequestParams();
             r.setQ(requestParams.getQ());
-            r.setFq(requestParams.getFq());
+            r.setFq(getFq(requestParams));
             r.setQc(requestParams.getQc());
             r.setPageSize(0);
             r.setFacet(false);
@@ -1903,8 +2035,8 @@
 
             ArrayList<String> forNulls = new ArrayList<String>(sz);
             String[] fqs = null;
-            String[] originalFqs = requestParams.getFq();
-            if (requestParams.getFq() == null || requestParams.getFq().length == 0) {
+            String[] originalFqs = getFq(requestParams);
+            if (originalFqs == null || originalFqs.length == 0) {
                 fqs = new String[1];
             } else {
                 fqs = new String[originalFqs.length + 1];
@@ -1986,6 +2118,39 @@
         }
     }
 
+    private String[] getFq(SpatialSearchRequestParams requestParams) {
+        int requestParamsFqLength = requestParams.getFq() != null ? requestParams.getFq().length : 0;
+
+        String [] qidFq = null;
+        int qidFqLength = 0;
+        String q = requestParams.getQ();
+        if (q.startsWith("qid:")) {
+            try {
+                qidFq = ParamsCache.get(Long.parseLong(q.substring(4))).getFqs();
+                if (qidFq != null) {
+                    qidFqLength = qidFq.length;
+                }
+            } catch (Exception e) {
+            }
+        }
+
+        if (requestParamsFqLength + qidFqLength == 0) {
+            return null;
+        }
+
+        String [] allFqs = new String[requestParamsFqLength + qidFqLength];
+
+        if (requestParamsFqLength > 0) {
+            System.arraycopy(requestParams.getFq(), 0, allFqs, 0, requestParamsFqLength);
+        }
+
+        if (qidFqLength > 0) {
+            System.arraycopy(qidFq, 0, allFqs, requestParamsFqLength, qidFqLength);
+        }
+
+        return allFqs;
+    }
+
     /**
      * TODO remove code duplicate between wmsUncached and wmsCached
      *
@@ -1996,7 +2161,8 @@
             WmsEnv vars, PointType pointType, double[] pbbox,
             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) throws Exception {
+            String[] boundingBoxFqs, boolean outlinePoints, String outlineColour, HttpServletResponse response,
+            boolean is4326, double[] tilebbox) throws Exception {
 
         //colour mapping
         List<LegendItem> colours = (vars.colourMode.equals("-1") || vars.colourMode.equals("grid")) ? null : getColours(requestParams, vars.colourMode);
@@ -2008,7 +2174,7 @@
         List<String> forNulls = new ArrayList<String>(sz);
         String[] fqs = null;
         String[] origAndBBoxFqs = null;
-        if (requestParams.getFq() == null || requestParams.getFq().length == 0) {
+        if (originalFqs == null || originalFqs.length == 0) {
             fqs = new String[3];
             fqs[1] = boundingBoxFqs[0];
             fqs[2] = boundingBoxFqs[1];
@@ -2057,7 +2223,16 @@
             } else if (colours.size() >= colourList.length - 1) {
                 fqs = new String[forNulls.size()];
                 forNulls.toArray(fqs);
-                requestParams.setFq(fqs);
+
+                String [] newFqs = new String[originalFqs.length + forNulls.size()];
+                if (originalFqs.length > 0) {
+                    System.arraycopy(originalFqs, 0, newFqs, 0, originalFqs.length);
+                }
+                if (fqs.length > 0) {
+                    System.arraycopy(fqs, 0, newFqs, originalFqs.length, fqs.length);
+                }
+                requestParams.setFq(newFqs);
+
                 points.add(searchDAO.getFacetPoints(requestParams, pointType));
                 pColour.add(colourList[colourList.length - 1] | (vars.alpha << 24));
             }
@@ -2074,6 +2249,12 @@
         double grid_height_mult = (height / (pbbox[1] - pbbox[3])) / (height / divs);
         int x, y;
 
+        //for 4326
+        double top = tilebbox[3];
+        double bottom = tilebbox[1];
+        double left = tilebbox[0];
+        double right = tilebbox[2];
+
         for (int j = 0; j < points.size(); j++) {
 
             if(hq != null && hq.contains(j)){
@@ -2098,15 +2279,20 @@
                     float lng = pt.getCoordinates().get(0).floatValue();
                     float lat = pt.getCoordinates().get(1).floatValue();
 
-                    x = (int) ((convertLngToPixel(lng) - pbbox[0]) * grid_width_mult);
-                    y = (int) ((convertLatToPixel(lat) - pbbox[3]) * grid_height_mult);
+                    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] += pt.getCount();
                     }
                 }
             } else {
-                renderPoints(vars, pbbox, width_mult, height_mult, pointWidth, outlinePoints, outlineColour, pColour, imgObj, j, ps);
+                renderPoints(vars, pbbox, width_mult, height_mult, pointWidth, outlinePoints, outlineColour, pColour, imgObj, j, ps, is4326, tilebbox, height, width);
             }
         }
 
@@ -2132,31 +2318,42 @@
                 }
             }
         } else {
-            drawUncertaintyCircles(requestParams, vars, height, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs);
+            drawUncertaintyCircles(requestParams, vars, height, width, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs, is4326, tilebbox);
         }
 
         //highlight
         if (vars.highlight != null) {
-            imgObj = drawHighlight(requestParams, vars, pointType, width, height, pbbox, width_mult, height_mult, imgObj, originalFqs, boundingBoxFqs);
+            imgObj = drawHighlight(requestParams, vars, pointType, width, height, pbbox, width_mult, height_mult, imgObj, originalFqs, boundingBoxFqs, is4326, tilebbox);
         }
 
         return imgObj;
     }
 
-    private void renderPoints(WmsEnv vars, double[] pbbox, double width_mult, double height_mult, int pointWidth, boolean outlinePoints, String outlineColour, List<Integer> pColour, ImgObj imgObj, int j, List<OccurrencePoint> ps) {
+    private void renderPoints(WmsEnv vars, double[] pbbox, double width_mult, double height_mult, int pointWidth, boolean outlinePoints, String outlineColour, List<Integer> pColour, ImgObj imgObj, int j, List<OccurrencePoint> 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.size(); i++) {
             OccurrencePoint pt = ps.get(i);
             float lng = pt.getCoordinates().get(0).floatValue();
             float lat = pt.getCoordinates().get(1).floatValue();
 
-            x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-            y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
+            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);
Index: pom.xml
===================================================================
--- pom.xml	(revision 4551)
+++ pom.xml	(revision 4552)
@@ -359,6 +359,12 @@
             <artifactId>commons-httpclient</artifactId>
             <version>3.1</version>
         </dependency>
+        <!-- charting -->
+        <dependency>
+            <groupId>jfree</groupId>
+            <artifactId>jfreechart</artifactId>
+            <version>1.0.13</version>
+        </dependency>
     </dependencies>
     <repositories>
         <!-- ALA repository  -->
Index: src/main/java/au/org/ala/biocache/web/ScatterplotController.java
===================================================================
--- src/main/java/au/org/ala/biocache/web/ScatterplotController.java	(revision 0)
+++ src/main/java/au/org/ala/biocache/web/ScatterplotController.java	(revision 4552)
@@ -0,0 +1,230 @@
+package au.org.ala.biocache.web;
+
+import au.org.ala.biocache.dao.SearchDAO;
+import au.org.ala.biocache.dto.IndexFieldDTO;
+import au.org.ala.biocache.dto.SearchResultDTO;
+import au.org.ala.biocache.dto.SpatialSearchRequestParams;
+import org.apache.log4j.Logger;
+import org.apache.solr.common.SolrDocumentList;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartRenderingInfo;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.encoders.EncoderUtil;
+import org.jfree.chart.encoders.ImageFormat;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.data.xy.DefaultXYDataset;
+import org.jfree.ui.RectangleEdge;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletResponse;
+import java.awt.*;
+import java.awt.geom.Ellipse2D;
+import java.awt.image.BufferedImage;
+import java.util.*;
+
+/**
+ * This controller is responsible for providing basic scatterplot services.
+ *
+ * Basic scatterplot is
+ * - occurrences, standard biocache query
+ * - x, numerical stored value field
+ * - y, numerical stored value field
+ * - height, integer default 256
+ * - width, integer default 256
+ * - title, string default query-display-name
+ * - pointcolour, colour as RGB string like FF0000 for red, default 0000FF
+ * - pointradius, double default 3
+ *
+ */
+@Controller
+public class ScatterplotController {
+
+    private static Logger logger = Logger.getLogger(ScatterplotController.class);
+
+    private final static int PAGE_SIZE = 100000000;
+    private final static String DEFAULT_SCATTERPLOT_TITLE = " ";
+    private final static String DEFAULT_SCATTERPLOT_HEIGHT = "256";
+    private final static String DEFAULT_SCATTERPLOT_WIDTH = "256";
+    private final static String DEFAULT_SCATTERPLOT_POINTCOLOUR = "0000FF";
+    private final static String DEFAULT_SCATTERPLOT_POINTRADIUS = "3";
+    private final static String [] VALID_DATATYPES = {"double","int","long"};
+
+    @Inject
+    protected SearchDAO searchDAO;
+
+    @RequestMapping(value = {"/scatterplot"}, method = RequestMethod.GET)
+    public void scatterplot(SpatialSearchRequestParams requestParams,
+                            @RequestParam(value = "x", required = true) String x,
+                            @RequestParam(value = "y", required = true) String y,
+                            @RequestParam(value = "height", required = false, defaultValue=DEFAULT_SCATTERPLOT_HEIGHT) Integer height,
+                            @RequestParam(value = "width", required = false, defaultValue=DEFAULT_SCATTERPLOT_WIDTH) Integer width,
+                            @RequestParam(value = "title", required = false, defaultValue=DEFAULT_SCATTERPLOT_TITLE) String title,
+                            @RequestParam(value = "pointcolour", required = false, defaultValue=DEFAULT_SCATTERPLOT_POINTCOLOUR) String pointcolour,
+                            @RequestParam(value = "pointradius", required = false, defaultValue = DEFAULT_SCATTERPLOT_POINTRADIUS) Double pointradius,
+                            HttpServletResponse response) throws Exception {
+        JFreeChart jChart = makeScatterplot(requestParams, x, y, title, pointcolour, pointradius);
+
+        //produce image
+        ChartRenderingInfo chartRenderingInfo = new ChartRenderingInfo();
+        BufferedImage bi = jChart.createBufferedImage(width, height, BufferedImage.TRANSLUCENT, chartRenderingInfo);
+        byte[] bytes = EncoderUtil.encode(bi, ImageFormat.PNG, true);
+
+        //output image
+        response.setContentType("image/png");
+        response.getOutputStream().write(bytes);
+    }
+
+    @RequestMapping(value = {"/scatterplot/point"}, method = RequestMethod.GET)
+    public Map scatterplotPointInfo(SpatialSearchRequestParams requestParams,
+                            @RequestParam(value = "x", required = true) String x,
+                            @RequestParam(value = "y", required = true) String y,
+                            @RequestParam(value = "height", required = false, defaultValue=DEFAULT_SCATTERPLOT_HEIGHT) Integer height,
+                            @RequestParam(value = "width", required = false, defaultValue=DEFAULT_SCATTERPLOT_WIDTH) Integer width,
+                            @RequestParam(value = "title", required = false, defaultValue=DEFAULT_SCATTERPLOT_TITLE) String title,
+                            @RequestParam(value = "pointx1", required = true) Integer pointx1,
+                            @RequestParam(value = "pointy1", required = true) Integer pointy1,
+                            @RequestParam(value = "pointx2", required = true) Integer pointx2,
+                            @RequestParam(value = "pointy2", required = true) Integer pointy2) throws Exception {
+
+        JFreeChart jChart = makeScatterplot(requestParams, x, y, title, "000000", 1.0);
+
+        //produce image
+        ChartRenderingInfo chartRenderingInfo = new ChartRenderingInfo();
+        BufferedImage bi = jChart.createBufferedImage(width, height, BufferedImage.TRANSLUCENT, chartRenderingInfo);
+
+        XYPlot plot = (XYPlot) jChart.getPlot();
+
+        //identify point range across x and y
+        double tx1 = plot.getRangeAxis().java2DToValue(pointx1, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.BOTTOM);
+        double tx2 = plot.getRangeAxis().java2DToValue(pointx2, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.BOTTOM);
+        double ty1 = plot.getDomainAxis().java2DToValue(pointy1, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.LEFT);
+        double ty2 = plot.getDomainAxis().java2DToValue(pointy2, chartRenderingInfo.getPlotInfo().getDataArea(), RectangleEdge.LEFT);
+        double x1 = Math.min(tx1, tx2);
+        double x2 = Math.max(tx1, tx2);
+        double y1 = Math.min(ty1, ty2);
+        double y2 = Math.max(ty1, ty2);
+
+        Map map = new HashMap();
+        map.put("xaxis_pixel_selection",new int[] {pointx1, pointx2});
+        map.put("yaxis_pixel_selection",new int[] {pointy1, pointy2});
+        map.put("xaxis",x);
+        map.put("yaxis",y);
+        map.put("xaxis_range",new double[]{x1, x2});
+        map.put("yaxis_range",new double[]{y1, y2});
+
+        return map;
+
+        /*
+        //add new fqs
+        String [] fqs_old = requestParams.getFq();
+        String [] fqs_new = new String[fqs_old.length + 2];
+        System.arraycopy(fqs_old,0,fqs_new,0,fqs_old.length);
+        fqs_new[fqs_old.length] = x + ":[" + x1 + " TO " + x2 + "]";
+        fqs_new[fqs_old.length + 1] = y + ":[" + y1 + " TO " + y2 + "]";
+        requestParams.setFq(fqs_new);
+        return searchDAO.findByFulltextSpatialQuery(requestParams, null);
+        */
+    }
+
+    JFreeChart makeScatterplot(SpatialSearchRequestParams requestParams, String x, String y
+        , String title, String pointcolour, Double pointradius) throws Exception {
+        //verify x and y are numerical and stored
+        String displayNameX = null;
+        String displayNameY = null;
+        for (IndexFieldDTO indexFieldDTO : searchDAO.getIndexFieldDetails(new String[]{x}) ){
+            if (!Arrays.asList(VALID_DATATYPES).contains(indexFieldDTO.getDataType() )) {
+                throw new Exception("Invalid datatype: " + indexFieldDTO.getDataType() + " for x: " + x);
+            }
+            if(!indexFieldDTO.isStored()) {
+                throw new Exception("Cannot use x: " + x + ".  It is not a stored field.");
+            }
+            displayNameX = indexFieldDTO.getDescription();
+        }
+        for (IndexFieldDTO indexFieldDTO : searchDAO.getIndexFieldDetails(new String[]{y}) ){
+            if (!Arrays.asList(VALID_DATATYPES).contains(indexFieldDTO.getDataType() )) {
+                throw new Exception("Invalid datatype: " + indexFieldDTO.getDataType() + " for y: " + y);
+            }
+            if(!indexFieldDTO.isStored()) {
+                throw new Exception("Cannot use y: " + y + ".  It is not a stored field.");
+            }
+            displayNameY = indexFieldDTO.getDescription();
+        }
+        if(displayNameX == null) {
+            throw new Exception("Unknown value for x: " + x);
+        }
+        if(displayNameY == null) {
+            throw new Exception("Unknown value for y: " + y);
+        }
+
+        //get data
+        requestParams.setPageSize(PAGE_SIZE);
+        requestParams.setFl(x + "," + y);
+        SolrDocumentList sdl = searchDAO.findByFulltext(requestParams);
+        int size = sdl.size();
+        double [][] data = new double[2][size];
+        int count = 0;
+        for(int i=0;i<size;i++) {
+            try {
+                Object a = sdl.get(i).getFieldValue(y);
+                Object b = sdl.get(i).getFieldValue(x);
+                data[1][i] = Double.parseDouble(String.valueOf(sdl.get(i).getFieldValue(y)));
+                if(a instanceof Double) {
+                    data[0][i] = (Double) a;
+                } else {
+                    data[0][i] = Double.parseDouble(String.valueOf(a));
+                }
+
+                if(b instanceof Double) {
+                    data[1][i] = (Double) b;
+                } else {
+                    data[1][i] = Double.parseDouble(String.valueOf(b));
+                }
+
+                count++;
+            } catch (Exception e) {
+                data[0][i] = Double.NaN;
+                data[1][i] = Double.NaN;
+            }
+        }
+
+        if(count == 0) {
+            throw new Exception("valid records found for these input parameters");
+        }
+
+        //create dataset
+        DefaultXYDataset xyDataset = new DefaultXYDataset();
+        xyDataset.addSeries("series", data);
+
+        //create chart
+        JFreeChart jChart = ChartFactory.createScatterPlot(
+                title.equals(" ")?requestParams.getDisplayString():title //chart display name
+                , displayNameX //x-axis display name
+                , displayNameY //y-axis display name
+                , xyDataset
+                , PlotOrientation.HORIZONTAL, false, false, false);
+        jChart.setBackgroundPaint(Color.white);
+
+        //styling
+        XYPlot plot = (XYPlot) jChart.getPlot();
+        Font axisfont = new Font("Arial", Font.PLAIN, 10);
+        Font titlefont = new Font("Arial", Font.BOLD, 11);
+        plot.getDomainAxis().setLabelFont(axisfont);
+        plot.getDomainAxis().setTickLabelFont(axisfont);
+        plot.getRangeAxis().setLabelFont(axisfont);
+        plot.getRangeAxis().setTickLabelFont(axisfont);
+        plot.setBackgroundPaint(new Color(220, 220, 220));
+        jChart.getTitle().setFont(titlefont);
+
+        //point shape and colour
+        Color c = new Color(Integer.parseInt(pointcolour, 16));
+        plot.getRenderer().setSeriesPaint(0, c);
+        plot.getRenderer().setSeriesShape(0, new Ellipse2D.Double(-pointradius, -pointradius, pointradius*2, pointradius*2));
+
+        return jChart;
+    }
+}
Index: src/main/java/au/org/ala/biocache/web/WMSController.java
===================================================================
--- src/main/java/au/org/ala/biocache/web/WMSController.java	(revision 4535)
+++ src/main/java/au/org/ala/biocache/web/WMSController.java	(revision 4552)
@@ -152,7 +152,7 @@
         //store the title if necessary
         if(title == null)
             title = requestParams.getDisplayString();
-        String[] fqs = requestParams.getFq();
+        String[] fqs = getFq(requestParams);
         if(fqs != null && fqs.length==1 && fqs[0].length()==0){
             fqs =null;
         }
@@ -537,6 +537,14 @@
                 / (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);
     }
@@ -606,11 +614,12 @@
      * @param pbbox  the pbbox to initialise
      * @return
      */
-    private double getBBoxes(String bboxString, int width, int height, int size, boolean uncertainty, double[] mbbox, double[] bbox, double[] pbbox) {
+    private double getBBoxes(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 {
-                mbbox[i] = Double.parseDouble(s);
+                tilebbox[i] = Double.parseDouble(s);
+                mbbox[i] = tilebbox[i];
                 i++;
             } catch (Exception e) {
                 logger.error("Problem parsing BBOX: '" + bboxString + "'", e);
@@ -658,6 +667,75 @@
         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;
+
+        //check offset for points bb by maximum uncertainty
+        if (uncertainty) {
+            //estimate 0.01 degrees is 1000m
+            double scale = 0.01 / 1000;
+            if (xoffset < MAX_UNCERTAINTY * scale) {
+                xoffset = MAX_UNCERTAINTY * scale;
+            }
+            if (yoffset < MAX_UNCERTAINTY * scale) {
+                yoffset = MAX_UNCERTAINTY * scale;
+            }
+        }
+
+        //adjust offset for pixel height/width
+        xoffset += pixelWidth;
+        yoffset += pixelHeight;
+
+        /* not required for 4326
+        pbbox[0] = convertLngToPixel(convertMetersToLng(mbbox[0]));
+        pbbox[1] = convertLatToPixel(convertMetersToLat(mbbox[1]));
+        pbbox[2] = convertLngToPixel(convertMetersToLng(mbbox[2]));
+        pbbox[3] = convertLatToPixel(convertMetersToLat(mbbox[3]));
+        */
+
+        //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:");
@@ -697,7 +775,7 @@
             SpatialSearchRequestParams requestParams = new SpatialSearchRequestParams();
             requestParams.setQ(request.getQ());
             requestParams.setQc(request.getQc());
-            requestParams.setFq(request.getFq());
+            requestParams.setFq(getFq(request));
 
             //test for cutpoints on the back of colourMode
             String[] s = colourMode.split(",");
@@ -785,7 +863,7 @@
         String[] dir = {"asc", "asc", "desc", "desc"};
 
         //remove instances of null longitude or latitude
-        String[] fq = (String[]) ArrayUtils.addAll(requestParams.getFq(), new String[]{"longitude:[* TO *]", "latitude:[* TO *]"});
+        String[] fq = (String[]) ArrayUtils.addAll(getFq(requestParams), new String[]{"longitude:[* TO *]", "latitude:[* TO *]"});
         requestParams.setFq(fq);
         requestParams.setPageSize(10);
 
@@ -959,10 +1037,11 @@
         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);
+        double resolution = getBBoxes(bboxString, width, height, size, vars.uncertainty, mbbox, bbox, pbbox, tilebbox);
 
         //resolution should be a value < 1
         PointType pointType = getPointTypeForDegreesPerPixel(resolution);
@@ -1401,16 +1480,24 @@
         response.setHeader("Cache-Control", "max-age=86400"); //age == 1 day
         response.setContentType("image/png"); //only png images generated
 
-        if("EPSG:4326".equals(srs))
-            bboxString = convertBBox4326To900913(bboxString);    // to work around a UDIG bug
-
+        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];
         int size = vars.size + (vars.highlight != null ? HIGHLIGHT_RADIUS * 2 + (int) (vars.size * 0.2) : 0) + 5;  //bounding box buffer
 
-        double resolution = getBBoxes(bboxString, width, height, size, vars.uncertainty, mbbox, bbox, pbbox);
+        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());
 
@@ -1421,7 +1508,7 @@
             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]);
@@ -1434,6 +1521,8 @@
         //build request
         if (q.length() > 0) {
             requestParams.setQ(q);
+        } else {
+            q = requestParams.getQ();
         }
 
         //bounding box test (q must be 'qid:' + number)
@@ -1446,7 +1535,7 @@
             }
         }
 
-        String[] originalFqs = requestParams.getFq();
+        String[] originalFqs = getFq(requestParams);
 
         //get from cache
         WMSTile wco = null;
@@ -1460,11 +1549,11 @@
         if (wco == null) {
             imgObj = wmsUncached(requestParams, vars, pointType, pbbox, mbbox,
                     width, height, width_mult, height_mult, pointWidth,
-                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response);
+                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response, is4326, tilebbox);
         } else {
             imgObj = wmsCached(wco, requestParams, vars, pointType, pbbox, bbox, mbbox,
                     width, height, width_mult, height_mult, pointWidth,
-                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response);
+                    originalFqs, hq, boundingBoxFqs, outlinePoints, outlineColour, response, is4326, tilebbox);
         }
 
         if (imgObj != null && imgObj.g != null) {
@@ -1638,7 +1727,8 @@
                              double height_mult, int pointWidth, String[] originalFqs, Set<Integer> hq,
                              String[] boundingBoxFqs, boolean outlinePoints,
                              String outlineColour,
-                             HttpServletResponse response) throws Exception {
+                             HttpServletResponse response,
+                             boolean is4326, double [] tilebbox) throws Exception {
 
         ImgObj imgObj = null;
 
@@ -1677,6 +1767,12 @@
                     continue;
                 }
 
+                //for 4326
+                double top = tilebbox[3];
+                double bottom = tilebbox[1];
+                double left = tilebbox[0];
+                double right = tilebbox[2];
+
                 if (vars.colourMode.equals("grid")) {
                     //render grids
                     int[] count = counts.get(j);
@@ -1687,9 +1783,16 @@
                         float lat = ps[i + 1];
                         if (lng >= bbox[0] && lng <= bbox[2]
                                 && lat >= bbox[1] && lat <= bbox[3]) {
-                            x = (int) ((convertLngToPixel(lng) - pbbox[0]) * grid_width_mult);
-                            y = (int) ((convertLatToPixel(lat) - pbbox[3]) * grid_height_mult);
 
+
+                            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];
                             }
@@ -1705,9 +1808,15 @@
                         float lat = ps[i + 1];
                         if (lng >= bbox[0] && lng <= bbox[2]
                                 && lat >= bbox[1] && lat <= bbox[3]) {
-                            x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-                            y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
 
+                            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.fillOval(x - vars.size, y - vars.size, pointWidth, pointWidth);
                             if(outlinePoints){
                                 imgObj.g.setPaint(oColour);
@@ -1742,18 +1851,18 @@
                 }
             }
         } else {
-            drawUncertaintyCircles(requestParams, vars, height, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs);
+            drawUncertaintyCircles(requestParams, vars, height, width, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs, is4326, tilebbox);
         }
 
         //highlight
         if (vars.highlight != null) {
-            imgObj = drawHighlight(requestParams, vars, pointType, width, height, pbbox, width_mult, height_mult, imgObj, originalFqs, boundingBoxFqs);
+            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, double[] pbbox, double[] mbbox, double width_mult, double height_mult, Graphics2D g, String[] originalFqs, String[] boundingBoxFqs) throws Exception {
+    void drawUncertaintyCircles(SpatialSearchRequestParams requestParams, WmsEnv vars, int height, int width, double[] pbbox, double[] mbbox, double width_mult, double height_mult, Graphics2D g, String[] originalFqs, String[] boundingBoxFqs, boolean is4326, double [] tilebbox) throws Exception {
         //draw uncertainty circles
         double hmult = (height / (mbbox[3] - mbbox[1]));
 
@@ -1787,6 +1896,12 @@
                 //TODO: paging
                 SolrDocumentList sdl = searchDAO.findByFulltext(requestParams);
 
+                //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);
@@ -1800,8 +1915,13 @@
                         lng = (Double) sdl.get(i).getFieldValue("longitude");
                         lat = (Double) sdl.get(i).getFieldValue("latitude");
 
-                        x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-                        y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
+                        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);
@@ -1816,7 +1936,7 @@
 
     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) throws Exception {
+                         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;
@@ -1837,13 +1957,25 @@
             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.size(); i++) {
                 OccurrencePoint pt = ps.get(i);
                 float lng = pt.getCoordinates().get(0).floatValue();
                 float lat = pt.getCoordinates().get(1).floatValue();
 
-                x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-                y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
+                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);
             }
@@ -1888,7 +2020,7 @@
             //points count
             SpatialSearchRequestParams r = new SpatialSearchRequestParams();
             r.setQ(requestParams.getQ());
-            r.setFq(requestParams.getFq());
+            r.setFq(getFq(requestParams));
             r.setQc(requestParams.getQc());
             r.setPageSize(0);
             r.setFacet(false);
@@ -1903,8 +2035,8 @@
 
             ArrayList<String> forNulls = new ArrayList<String>(sz);
             String[] fqs = null;
-            String[] originalFqs = requestParams.getFq();
-            if (requestParams.getFq() == null || requestParams.getFq().length == 0) {
+            String[] originalFqs = getFq(requestParams);
+            if (originalFqs == null || originalFqs.length == 0) {
                 fqs = new String[1];
             } else {
                 fqs = new String[originalFqs.length + 1];
@@ -1986,6 +2118,39 @@
         }
     }
 
+    private String[] getFq(SpatialSearchRequestParams requestParams) {
+        int requestParamsFqLength = requestParams.getFq() != null ? requestParams.getFq().length : 0;
+
+        String [] qidFq = null;
+        int qidFqLength = 0;
+        String q = requestParams.getQ();
+        if (q.startsWith("qid:")) {
+            try {
+                qidFq = ParamsCache.get(Long.parseLong(q.substring(4))).getFqs();
+                if (qidFq != null) {
+                    qidFqLength = qidFq.length;
+                }
+            } catch (Exception e) {
+            }
+        }
+
+        if (requestParamsFqLength + qidFqLength == 0) {
+            return null;
+        }
+
+        String [] allFqs = new String[requestParamsFqLength + qidFqLength];
+
+        if (requestParamsFqLength > 0) {
+            System.arraycopy(requestParams.getFq(), 0, allFqs, 0, requestParamsFqLength);
+        }
+
+        if (qidFqLength > 0) {
+            System.arraycopy(qidFq, 0, allFqs, requestParamsFqLength, qidFqLength);
+        }
+
+        return allFqs;
+    }
+
     /**
      * TODO remove code duplicate between wmsUncached and wmsCached
      *
@@ -1996,7 +2161,8 @@
             WmsEnv vars, PointType pointType, double[] pbbox,
             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) throws Exception {
+            String[] boundingBoxFqs, boolean outlinePoints, String outlineColour, HttpServletResponse response,
+            boolean is4326, double[] tilebbox) throws Exception {
 
         //colour mapping
         List<LegendItem> colours = (vars.colourMode.equals("-1") || vars.colourMode.equals("grid")) ? null : getColours(requestParams, vars.colourMode);
@@ -2008,7 +2174,7 @@
         List<String> forNulls = new ArrayList<String>(sz);
         String[] fqs = null;
         String[] origAndBBoxFqs = null;
-        if (requestParams.getFq() == null || requestParams.getFq().length == 0) {
+        if (originalFqs == null || originalFqs.length == 0) {
             fqs = new String[3];
             fqs[1] = boundingBoxFqs[0];
             fqs[2] = boundingBoxFqs[1];
@@ -2057,7 +2223,16 @@
             } else if (colours.size() >= colourList.length - 1) {
                 fqs = new String[forNulls.size()];
                 forNulls.toArray(fqs);
-                requestParams.setFq(fqs);
+
+                String [] newFqs = new String[originalFqs.length + forNulls.size()];
+                if (originalFqs.length > 0) {
+                    System.arraycopy(originalFqs, 0, newFqs, 0, originalFqs.length);
+                }
+                if (fqs.length > 0) {
+                    System.arraycopy(fqs, 0, newFqs, originalFqs.length, fqs.length);
+                }
+                requestParams.setFq(newFqs);
+
                 points.add(searchDAO.getFacetPoints(requestParams, pointType));
                 pColour.add(colourList[colourList.length - 1] | (vars.alpha << 24));
             }
@@ -2074,6 +2249,12 @@
         double grid_height_mult = (height / (pbbox[1] - pbbox[3])) / (height / divs);
         int x, y;
 
+        //for 4326
+        double top = tilebbox[3];
+        double bottom = tilebbox[1];
+        double left = tilebbox[0];
+        double right = tilebbox[2];
+
         for (int j = 0; j < points.size(); j++) {
 
             if(hq != null && hq.contains(j)){
@@ -2098,15 +2279,20 @@
                     float lng = pt.getCoordinates().get(0).floatValue();
                     float lat = pt.getCoordinates().get(1).floatValue();
 
-                    x = (int) ((convertLngToPixel(lng) - pbbox[0]) * grid_width_mult);
-                    y = (int) ((convertLatToPixel(lat) - pbbox[3]) * grid_height_mult);
+                    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] += pt.getCount();
                     }
                 }
             } else {
-                renderPoints(vars, pbbox, width_mult, height_mult, pointWidth, outlinePoints, outlineColour, pColour, imgObj, j, ps);
+                renderPoints(vars, pbbox, width_mult, height_mult, pointWidth, outlinePoints, outlineColour, pColour, imgObj, j, ps, is4326, tilebbox, height, width);
             }
         }
 
@@ -2132,31 +2318,42 @@
                 }
             }
         } else {
-            drawUncertaintyCircles(requestParams, vars, height, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs);
+            drawUncertaintyCircles(requestParams, vars, height, width, pbbox, mbbox, width_mult, height_mult, imgObj.g, originalFqs, boundingBoxFqs, is4326, tilebbox);
         }
 
         //highlight
         if (vars.highlight != null) {
-            imgObj = drawHighlight(requestParams, vars, pointType, width, height, pbbox, width_mult, height_mult, imgObj, originalFqs, boundingBoxFqs);
+            imgObj = drawHighlight(requestParams, vars, pointType, width, height, pbbox, width_mult, height_mult, imgObj, originalFqs, boundingBoxFqs, is4326, tilebbox);
         }
 
         return imgObj;
     }
 
-    private void renderPoints(WmsEnv vars, double[] pbbox, double width_mult, double height_mult, int pointWidth, boolean outlinePoints, String outlineColour, List<Integer> pColour, ImgObj imgObj, int j, List<OccurrencePoint> ps) {
+    private void renderPoints(WmsEnv vars, double[] pbbox, double width_mult, double height_mult, int pointWidth, boolean outlinePoints, String outlineColour, List<Integer> pColour, ImgObj imgObj, int j, List<OccurrencePoint> 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.size(); i++) {
             OccurrencePoint pt = ps.get(i);
             float lng = pt.getCoordinates().get(0).floatValue();
             float lat = pt.getCoordinates().get(1).floatValue();
 
-            x = (int) ((convertLngToPixel(lng) - pbbox[0]) * width_mult);
-            y = (int) ((convertLatToPixel(lat) - pbbox[3]) * height_mult);
+            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);
Index: pom.xml
===================================================================
--- pom.xml	(revision 4535)
+++ pom.xml	(revision 4552)
@@ -359,6 +359,12 @@
             <artifactId>commons-httpclient</artifactId>
             <version>3.1</version>
         </dependency>
+        <!-- charting -->
+        <dependency>
+            <groupId>jfree</groupId>
+            <artifactId>jfreechart</artifactId>
+            <version>1.0.13</version>
+        </dependency>
     </dependencies>
     <repositories>
         <!-- ALA repository  -->