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

Java tutorial

Introduction

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

Source

/**************************************************************************
 *  Copyright (C) 2010 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.Store;
import au.org.ala.biocache.dao.SearchDAO;
import au.org.ala.biocache.dao.SearchDAOImpl;
import au.org.ala.biocache.dto.*;
import au.org.ala.biocache.util.ParamsCache;
import au.org.ala.biocache.util.ParamsCacheObject;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import javax.inject.Inject;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.util.*;

/**
 * Controller for the "explore your area" page
 *
 * @author "Nick dos Remedios <Nick.dosRemedios@csiro.au>"
 * @author "Natasha Carter <natasha.carter@csiro.au>"
 */
@Controller("exploreController")
public class ExploreController {

    /** Logger initialisation */
    private final static Logger logger = Logger.getLogger(ExploreController.class);

    /** Fulltext search DAO */
    @Inject
    protected SearchDAO searchDao;
    /** Name of view for site home page */
    private final String DEFAULT_LOCATION = "Clunies Ross St, Black Mountain, ACT";
    private final String POINTS_GEOJSON = "json/pointsGeoJson";

    /** Mapping of radius in km to OpenLayers zoom level */
    public final static HashMap<Float, Integer> radiusToZoomLevelMap = new HashMap<Float, Integer>();
    static {
        radiusToZoomLevelMap.put(1f, 14);
        radiusToZoomLevelMap.put(5f, 12);
        radiusToZoomLevelMap.put(10f, 11);
        radiusToZoomLevelMap.put(50f, 9);
    }

    @RequestMapping(value = "/explore/hierarchy", method = RequestMethod.GET)
    public void getHierarchy(HttpServletResponse response) throws Exception {
        response.setContentType("application/json");
        OutputStream out = response.getOutputStream();
        out.write(Store.retrieveSubgroupsConfig().getBytes("UTF-8"));
        out.flush();
        out.close();
    }

    /**
     * Returns a hierarchical listing of species groups.
     *
     * TODO push down to service implementation.
     *
     * @param requestParams
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/explore/hierarchy/groups*", method = RequestMethod.GET)
    public @ResponseBody Collection<SpeciesGroupDTO> yourHierarchicalAreaView(
            SpatialSearchRequestParams requestParams, String speciesGroup) throws Exception {

        List<au.org.ala.biocache.vocab.SpeciesGroup> ssgs = au.org.ala.biocache.Store.retrieveSpeciesSubgroups();
        Map<String, SpeciesGroupDTO> parentGroupMap = new LinkedHashMap<String, SpeciesGroupDTO>();

        //create a parent lookup table
        Map<String, String> parentLookup = new HashMap<String, String>();
        for (au.org.ala.biocache.vocab.SpeciesGroup sg : ssgs) {
            if (sg.parent() != null && sg.parent() != "") {
                parentLookup.put(sg.name().toLowerCase(), sg.parent());
                if (parentGroupMap.get(sg.parent()) == null) {
                    parentGroupMap.put(sg.parent(), new SpeciesGroupDTO(sg.parent(), 0, 0, 1));
                }
            }
        }

        //get the species group occurrence counts
        requestParams.setFormattedQuery(null);
        requestParams.setFacets(new String[] { "species_subgroup" });
        requestParams.setPageSize(0);
        requestParams.setFlimit(-1);
        if (StringUtils.isNotBlank(speciesGroup)) {
            requestParams.setFq(new String[] { "species_group:\"" + speciesGroup + "\"" });
        }

        //retrieve a list of subgroups with occurrences matching the query
        SearchResultDTO speciesSubgroupCounts = searchDao.findByFulltextSpatialQuery(requestParams, null);
        Map<String, Long> occurrenceCounts = new HashMap<String, Long>();
        if (speciesSubgroupCounts.getFacetResults().size() > 0) {
            FacetResultDTO result = speciesSubgroupCounts.getFacetResults().iterator().next();
            for (FieldResultDTO fr : result.getFieldResult()) {
                occurrenceCounts.put(fr.getLabel(), fr.getCount());
            }
        }

        //do a facet query for each species subgroup
        for (String ssg : occurrenceCounts.keySet()) {

            requestParams.setQ("species_subgroup:\"" + ssg + "\"");
            requestParams.setFormattedQuery(null);
            requestParams.setFacets(new String[] { "taxon_name" });

            List<FacetResultDTO> facetResultDTO = searchDao.getFacetCounts(requestParams);
            if (facetResultDTO.size() > 0) {
                FacetResultDTO result = facetResultDTO.get(0);

                String parentName = parentLookup.get(ssg.toLowerCase());
                SpeciesGroupDTO parentGroup = parentGroupMap.get(parentName);
                if (parentGroup.getChildGroups() == null) {
                    parentGroup.setChildGroups(new ArrayList<SpeciesGroupDTO>());
                }
                parentGroup.getChildGroups()
                        .add(new SpeciesGroupDTO(ssg, result.getCount(), occurrenceCounts.get(ssg), 2));
                parentGroup.setSpeciesCount(parentGroup.getSpeciesCount() + result.getCount());
                parentGroup.setCount(parentGroup.getCount() + occurrenceCounts.get(ssg));
            }
        }

        //prune empty parents
        List<String> toRemove = new ArrayList<String>();
        for (String parentName : parentGroupMap.keySet()) {
            if (parentGroupMap.get(parentName).getChildGroups() == null
                    || parentGroupMap.get(parentName).getChildGroups().size() == 0) {
                toRemove.add(parentName);
            }
        }
        for (String key : toRemove) {
            parentGroupMap.remove(key);
        }

        return parentGroupMap.values();
    }

    /**
     *
     * Returns a list of species groups and counts that will need to be displayed.
     *
     * TODO: MOVE all the IP lat long lookup to the the client webapp.  The purposes
     * of biocache-service is to provide a service layer over the biocache.
     *
     */
    @RequestMapping(value = "/explore/groups*", method = RequestMethod.GET)
    public @ResponseBody List<SpeciesGroupDTO> yourAreaView(SpatialSearchRequestParams requestParams)
            throws Exception {

        //now we want to grab all the facets to get the counts associated with the species groups
        List<au.org.ala.biocache.vocab.SpeciesGroup> sgs = au.org.ala.biocache.Store.retrieveSpeciesGroups();
        List<SpeciesGroupDTO> speciesGroups = new java.util.ArrayList<SpeciesGroupDTO>();
        SpeciesGroupDTO all = new SpeciesGroupDTO();
        String originalQ = requestParams.getQ();
        all.setName("ALL_SPECIES");
        all.setLevel(0);
        Integer[] counts = getYourAreaCount(requestParams, "ALL_SPECIES");
        all.setCount(counts[0]);
        all.setSpeciesCount(counts[1]);
        speciesGroups.add(all);

        String oldName = null;
        String kingdom = null;
        //set the counts an indent levels for all the species groups
        for (au.org.ala.biocache.vocab.SpeciesGroup sg : sgs) {
            logger.debug("name: " + sg.name() + " parent: " + sg.parent());
            int level = 3;
            SpeciesGroupDTO sdto = new SpeciesGroupDTO();
            sdto.setName(sg.name());

            if (oldName != null && sg.parent() != null && sg.parent().equals(kingdom)) {
                level = 2;
            }

            oldName = sg.name();
            if (sg.parent() == null) {
                level = 1;
                kingdom = sg.name();
            }
            sdto.setLevel(level);
            //set the original query back to default to clean up after ourselves
            requestParams.setQ(originalQ);
            //query per group
            counts = getYourAreaCount(requestParams, sg.name());
            sdto.setCount(counts[0]);
            sdto.setSpeciesCount(counts[1]);
            speciesGroups.add(sdto);
        }
        return speciesGroups;
    }

    /**
     * Returns the number of records and distinct species in a particular species group
     * 
     * @param requestParams
     * @param group
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/explore/counts/group/{group}*", method = RequestMethod.GET)
    public @ResponseBody Integer[] getYourAreaCount(SpatialSearchRequestParams requestParams,
            @PathVariable(value = "group") String group) throws Exception {
        addGroupFilterToQuery(requestParams, group);
        requestParams.setPageSize(0);
        requestParams.setFacets(new String[] { "taxon_name" });
        requestParams.setFlimit(-1);
        SearchResultDTO results = searchDao.findByFulltextSpatialQuery(requestParams, null);
        Integer speciesCount = 0;
        if (results.getFacetResults().size() > 0) {
            speciesCount = results.getFacetResults().iterator().next().getFieldResult().size();
        }

        return new Integer[] { (int) results.getTotalRecords(), speciesCount };
    }

    /**
     * Updates the requestParams to take into account the provided species group
     * @param requestParams
     * @param group
     */
    private void addGroupFilterToQuery(SpatialSearchRequestParams requestParams, String group) {
        addFacetFilterToQuery(requestParams, "species_group", group);
    }

    /**
     * Updates the requestParams to take into account the provided species group
     * @param requestParams
     * @param facetValue
     */
    private void addFacetFilterToQuery(SpatialSearchRequestParams requestParams, String facetName,
            String facetValue) {
        StringBuilder sb = new StringBuilder();
        if (requestParams.getQ() != null && !requestParams.getQ().isEmpty())
            sb.append(requestParams.getQ());
        else {
            sb.append("*:*");
        }
        if (!facetValue.equals("ALL_SPECIES"))
            sb.append(" " + facetName + ":").append(facetValue);
        //now ignore the records that have been identified to a rank above species
        sb.append(" -rank:kingdom -rank:phylum -rank:class -rank:order -rank:family -rank:genus");
        //String query = sb.togroup.equals("ALL_SPECIES")? "*:*" : "species_group:" + group;
        requestParams.setQ(sb.toString());
        //don't care about the formatted query
        requestParams.setFormattedQuery(null);
    }

    /**
     * GeoJSON view of records as clusters of points within a specified radius of a given location
     *
     * This service will be used by explore your area.
     */
    @RequestMapping(value = "/geojson/radius-points", method = RequestMethod.GET)
    public String radiusPointsGeoJson(SpatialSearchRequestParams requestParams,
            @RequestParam(value = "zoom", required = false, defaultValue = "0") Integer zoomLevel,
            @RequestParam(value = "bbox", required = false) String bbox,
            @RequestParam(value = "group", required = false, defaultValue = "ALL_SPECIES") String speciesGroup,
            Model model) throws Exception {
        addGroupFilterToQuery(requestParams, speciesGroup);
        PointType pointType = PointType.POINT_00001; // default value for when zoom is null
        pointType = getPointTypeForZoomLevel(zoomLevel);
        logger.info("PointType for zoomLevel (" + zoomLevel + ") = " + pointType.getLabel());
        List<OccurrencePoint> points = searchDao.findRecordsForLocation(requestParams, pointType);
        logger.info("Points search for " + pointType.getLabel() + " - found: " + points.size());
        model.addAttribute("points", points);
        return POINTS_GEOJSON;
    }

    /**
     * Map a zoom level to a coordinate accuracy level
     *
     * @param zoomLevel
     * @return
     */
    protected PointType getPointTypeForZoomLevel(Integer zoomLevel) {
        PointType pointType = null;
        // Map zoom levels to lat/long accuracy levels
        if (zoomLevel != null) {
            if (zoomLevel >= 0 && zoomLevel <= 6) {
                // 0-6 levels
                pointType = PointType.POINT_1;
            } else if (zoomLevel > 6 && zoomLevel <= 8) {
                // 6-7 levels
                pointType = PointType.POINT_01;
            } else if (zoomLevel > 8 && zoomLevel <= 10) {
                // 8-9 levels
                pointType = PointType.POINT_001;
            } else if (zoomLevel > 10 && zoomLevel <= 13) {
                // 10-12 levels
                pointType = PointType.POINT_0001;
            } else if (zoomLevel > 13 && zoomLevel <= 15) {
                // 12-n levels
                pointType = PointType.POINT_00001;
            } else {
                // raw levels
                pointType = PointType.POINT_RAW;
            }
        }
        return pointType;
    }

    private void applyFacetForCounts(SpatialSearchRequestParams requestParams, boolean useCommonName) {
        if (useCommonName)
            requestParams.setFacets(new String[] { SearchDAOImpl.COMMON_NAME_AND_LSID });
        else
            requestParams.setFacets(new String[] { SearchDAOImpl.NAMES_AND_LSID });
    }

    /**
    * Occurrence search page uses SOLR JSON to display results
    *
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/explore/group/{group}/download*", method = RequestMethod.GET)
    public void yourAreaDownload(DownloadRequestParams requestParams, @PathVariable(value = "group") String group,
            @RequestParam(value = "common", required = false, defaultValue = "false") boolean common,
            HttpServletResponse response) throws Exception {
        String filename = requestParams.getFile() != null ? requestParams.getFile() : "data";
        logger.debug("Downloading the species in your area... ");
        response.setHeader("Cache-Control", "must-revalidate");
        response.setHeader("Pragma", "must-revalidate");
        response.setHeader("Content-Disposition", "attachment;filename=" + filename);
        response.setContentType("application/vnd.ms-excel");

        addGroupFilterToQuery(requestParams, group);
        applyFacetForCounts(requestParams, common);

        ServletOutputStream out = response.getOutputStream();
        int count = searchDao.writeSpeciesCountByCircleToStream(requestParams, group, out);
        logger.debug("Exported " + count + " species records in the requested area");

    }

    /**
     * JSON web service that returns a list of species and record counts for a given location search
     * and a higher taxa with rank. 
     *
     * @param model
     * @throws Exception
     */
    @RequestMapping(value = "/explore/group/{group}*", method = RequestMethod.GET)
    public @ResponseBody List<TaxaCountDTO> listSpeciesForHigherTaxa(SpatialSearchRequestParams requestParams,
            @PathVariable(value = "group") String group,
            @RequestParam(value = "common", required = false, defaultValue = "false") boolean common, Model model)
            throws Exception {

        addGroupFilterToQuery(requestParams, group);
        applyFacetForCounts(requestParams, common);

        return searchDao.findAllSpeciesByCircleAreaAndHigherTaxa(requestParams, group);
    }

    // The Endemism Web Services - Move these if they get too large...
    /**
     * Returns the number of distinct species that are in the supplied region.
     * @param requestParams
     * @param response
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/explore/counts/endemic*", method = RequestMethod.GET)
    public @ResponseBody int getSpeciesCountOnlyInWKT(SpatialSearchRequestParams requestParams,
            HttpServletResponse response) throws Exception {
        List list = getSpeciesOnlyInWKT(requestParams, response);
        if (list != null)
            return list.size();
        return 0;
    }

    /**
     * Returns the species that only have occurrences in the supplied WKT.
     * @return
     */
    @RequestMapping(value = "/explore/endemic/species*", method = RequestMethod.GET)
    public @ResponseBody List<FieldResultDTO> getSpeciesOnlyInWKT(SpatialSearchRequestParams requestParams,
            HttpServletResponse response) throws Exception {
        ParamsCacheObject pco = ParamsCache.getParamCacheObjectFromQuery(requestParams.getQ());
        String wkt = StringUtils.isNotBlank(requestParams.getWkt()) ? requestParams.getWkt() : pco.getWkt();
        if (pco != null) {
            requestParams.setQ(pco.getQ());
            requestParams.setWkt(pco.getWkt());
        }

        if (StringUtils.isNotBlank(wkt)) {
            if (requestParams.getFacets() != null && requestParams.getFacets().length == 1) {
                return searchDao.getEndemicSpecies(requestParams);
            } else {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Please supply only one facet.");
            }
        } else {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Please supply a WKT area.");
        }
        return null;
    }

    /**
     * Returns facet values that only occur in the supplied subQueryQid
     * and not in the parentQuery.
     *
     * The facet is defined in the parentQuery. Default facet is SearchDAOImpl.NAMES_AND_LSID
     *
     * If no requestParams defined the default q=geospatial_kosher:* is used.
     *
     * @return
     */
    @RequestMapping(value = "/explore/endemic/species/{subQueryQid}*", method = RequestMethod.GET)
    public @ResponseBody List<FieldResultDTO> getSpeciesOnlyInOneQuery(SpatialSearchRequestParams parentQuery,
            @PathVariable(value = "subQueryQid") Long subQueryQid, HttpServletResponse response) throws Exception {
        ParamsCacheObject qid = ParamsCache.getParamCacheObjectFromQuery("qid:" + subQueryQid);
        SpatialSearchRequestParams subQuery = new SpatialSearchRequestParams();
        subQuery.setQ(qid.getQ());
        subQuery.setFacets(qid.getFqs());
        subQuery.setWkt(qid.getWkt());

        if (parentQuery.getQ() == null) {
            parentQuery.setQ("geospatial_kosher:*");
        }
        if (parentQuery.getFacets() == null || parentQuery.getFacets().length == 0) {
            parentQuery.setFacets(new String[] { SearchDAOImpl.NAMES_AND_LSID });
        }

        if (subQuery != null) {
            if (parentQuery.getFacets() != null && parentQuery.getFacets().length == 1) {
                return searchDao.getSubquerySpeciesOnly(subQuery, parentQuery);
            } else {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Please supply only one facet.");
            }
        } else {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Please supply a valid sub query qid.");
        }
        return null;
    }

    /**
     * Returns the species that only have occurrences in the supplied WKT.
     * @return
     */
    @RequestMapping(value = "/explore/endemic/species.csv", method = RequestMethod.GET)
    public void getEndemicSpeciesCSV(SpatialSearchRequestParams requestParams, HttpServletResponse response)
            throws Exception {
        requestParams.setFacets(new String[] { SearchDAOImpl.NAMES_AND_LSID });
        requestParams.setFq((String[]) ArrayUtils.add(requestParams.getFq(), "species_guid:[* TO *]"));
        List<FieldResultDTO> list = getSpeciesOnlyInWKT(requestParams, response);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/plain");
        java.io.PrintWriter writer = response.getWriter();
        writer.write("Family,Scientific name,Common name,Taxon rank,LSID,# Occurrences");
        for (FieldResultDTO item : list) {
            String s = item.getLabel();
            if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 2)
                s = s.substring(1, s.length() - 1);
            String[] values = s.split("\\|", 6);
            if (values.length >= 5) {
                writer.write("\n" + values[4] + ",\"" + values[0] + "\",\"" + values[2] + "\",," + values[1] + ","
                        + item.getCount());
            }
        }
        writer.flush();
        writer.close();
    }

    /**
     * Returns facet values that only occur in the supplied subQueryQid
     * and not in the parentQuery.
     *
     * The facet is defined in the parentQuery. Default facet is SearchDAOImpl.NAMES_AND_LSID
     *
     * If no requestParams defined the default q=geospatial_kosher:* is used.
     *
     * @return
     */
    @RequestMapping(value = "/explore/endemic/species/{subQueryQid}.csv", method = RequestMethod.GET)
    public void getSpeciesOnlyInOneQueryCSV(SpatialSearchRequestParams parentQuery,
            @PathVariable(value = "subQueryQid") Long subQueryQid, HttpServletResponse response) throws Exception {
        ParamsCacheObject qid = ParamsCache.getParamCacheObjectFromQuery("qid:" + subQueryQid);
        SpatialSearchRequestParams subQuery = new SpatialSearchRequestParams();
        subQuery.setQ(qid.getQ());
        subQuery.setFacets(qid.getFqs());
        subQuery.setWkt(qid.getWkt());

        if (parentQuery.getQ() == null) {
            parentQuery.setQ("geospatial_kosher:*");
        }
        if (parentQuery.getFacets() == null || parentQuery.getFacets().length == 0) {
            parentQuery.setFacets(new String[] { SearchDAOImpl.NAMES_AND_LSID });
        }

        if (subQuery != null) {
            if (parentQuery.getFacets() != null && parentQuery.getFacets().length == 1) {
                List<FieldResultDTO> list = searchDao.getSubquerySpeciesOnly(subQuery, parentQuery);
                response.setCharacterEncoding("UTF-8");
                response.setContentType("text/plain");
                java.io.PrintWriter writer = response.getWriter();
                writer.write("Family,Scientific name,Common name,Taxon rank,LSID,# Occurrences");
                for (FieldResultDTO item : list) {
                    String s = item.getLabel();
                    if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 2)
                        s = s.substring(1, s.length() - 1);
                    String[] values = s.split("\\|", 6);
                    if (values.length >= 5) {
                        writer.write(
                                "\n" + values[4] + ",\"" + values[0].replace("\"", "\"\"").replace("\\", "\\\\")
                                        + "\"," + "\"" + values[2].replace("\"", "\"\"").replace("\\", "\\\\")
                                        + "\",," + values[1] + "," + item.getCount());
                    }
                }
                writer.flush();
                writer.close();
            } else {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Please supply only one facet.");
            }
        } else {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Please supply a valid sub query qid.");
        }
    }

    /**
     * @param searchDao the searchDao to set
     */
    public void setSearchDao(SearchDAO searchDao) {
        this.searchDao = searchDao;
    }
}