/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * 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. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.web.servlet; import java.awt.Color; import java.awt.Font; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.jfree.chart.StandardChartTheme; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.IntervalMarker; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.title.TextTitle; import org.jfree.data.time.Day; import org.jfree.data.time.Hour; import org.jfree.data.time.Minute; import org.jfree.data.time.RegularTimePeriod; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.openmrs.Concept; import org.openmrs.ConceptNumeric; import org.openmrs.Obs; import org.openmrs.Patient; import org.openmrs.api.APIException; import org.openmrs.api.context.Context; /** * This servlet returns an image graphing the numeric values for given concept(s). <br/> * <br/> * This servlet is currently mapped to a /showGraphServlet url in web.xml<br/> * <br/> * For an example of usage, see WEB-INF/view/portlets/patientGraphs.jsp <br/> * <br/> * The only url parameters that are required are "patientId" and "conceptId". */ public class ShowGraphServlet extends HttpServlet { public static final long serialVersionUID = 1231231L; private Log log = LogFactory.getLog(ShowGraphServlet.class); // private static final DateFormat Formatter = new SimpleDateFormat("MM/dd/yyyy"); // Supported mime types private static final String PNG_MIME_TYPE = "image/png"; private static final String JPG_MIME_TYPE = "image/jpeg"; private static final Color COLOR_ABNORMAL = new Color(255, 255, 0, 64); private static final Color COLOR_CRITICAL = new Color(255, 128, 128, 64); private static final Color COLOR_ERROR = new Color(255, 28, 28, 64); /** * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { JFreeChart chart = getChart(request); // get the height and width of the graph String widthString = request.getParameter("width"); String heightString = request.getParameter("height"); Integer width; Integer height; if (widthString != null && widthString.length() > 0) width = Integer.parseInt(widthString); else width = 500; if (heightString != null && heightString.length() > 0) height = Integer.parseInt(heightString); else height = 300; // get the requested mime type of the graph String mimeType = request.getParameter("mimeType"); if (mimeType == null) mimeType = PNG_MIME_TYPE; // Modify response to disable caching response.setHeader("Pragma", "No-cache"); response.setDateHeader("Expires", 0); response.setHeader("Cache-Control", "no-cache"); // Write chart out to response as image try { if (JPG_MIME_TYPE.equalsIgnoreCase(mimeType)) { response.setContentType(JPG_MIME_TYPE); ChartUtilities.writeChartAsJPEG(response.getOutputStream(), chart, width, height); } else if (PNG_MIME_TYPE.equalsIgnoreCase(mimeType)) { response.setContentType(PNG_MIME_TYPE); ChartUtilities.writeChartAsPNG(response.getOutputStream(), chart, width, height); } else { throw new APIException("Unsupported MIME type"); } } catch (IOException e) { // if its tomcat and the user simply navigated away from the page, don't throw an error if (e.getClass().getName().equals("org.apache.catalina.connector.ClientAbortException")) { // do nothing } else { log.error("Error class name: " + e.getClass().getName()); log.error("Unable to write chart", e); } } } // Add error handling above and remove this try/catch catch (Exception e) { log.error("An unknown expected exception was thrown while rendering a graph", e); } } /** * The main method for this class. It will create a JFreeChart object to be written to the * response. * * @param request the current request will all the parameters needed * @return JFreeChart object to be rendered * @should set value axis label to given units * @should set value axis label to concept numeric units if given units is null */ protected JFreeChart getChart(HttpServletRequest request) { // All available GET parameters String patientId = request.getParameter("patientId"); // required String conceptId1 = request.getParameter("conceptId"); // required String conceptId2 = request.getParameter("conceptId2"); String chartTitle = request.getParameter("chartTitle"); String seriesTitle1 = request.getParameter("seriesTitle1"); String seriesTitle2 = request.getParameter("seriesTitle2"); String units = request.getParameter("units"); String minRangeString = request.getParameter("minRange"); String maxRangeString = request.getParameter("maxRange"); String hideDate = request.getParameter("hideDate"); Patient patient = Context.getPatientService().getPatient(Integer.parseInt(patientId)); // Set date range to passed values, otherwise set a default date range to the last 12 months Calendar cal = Calendar.getInstance(); Date fromDate = getFromDate(request.getParameter("fromDate")); Date toDate = getToDate(request.getParameter("toDate")); // Swap if fromDate is after toDate if (fromDate.getTime() > toDate.getTime()) { Long temp = fromDate.getTime(); fromDate.setTime(toDate.getTime()); toDate.setTime(temp); } // Graph parameters Double minRange = null; Double maxRange = null; Double normalLow = null; Double normalHigh = null; Double criticalLow = null; Double criticalHigh = null; String timeAxisTitle = null; String rangeAxisTitle = null; boolean userSpecifiedMaxRange = false; boolean userSpecifiedMinRange = false; // Fetching obs List<Obs> observations1 = new ArrayList<Obs>(); List<Obs> observations2 = new ArrayList<Obs>(); Concept concept1 = null, concept2 = null; if (conceptId1 != null) concept1 = Context.getConceptService().getConcept(Integer.parseInt(conceptId1)); if (conceptId2 != null) concept2 = Context.getConceptService().getConcept(Integer.parseInt(conceptId2)); if (concept1 != null) { observations1 = Context.getObsService().getObservationsByPersonAndConcept(patient, concept1); chartTitle = concept1.getBestName(request.getLocale()).getName(); rangeAxisTitle = ((ConceptNumeric) concept1).getUnits(); minRange = ((ConceptNumeric) concept1).getLowAbsolute(); maxRange = ((ConceptNumeric) concept1).getHiAbsolute(); normalLow = ((ConceptNumeric) concept1).getLowNormal(); normalHigh = ((ConceptNumeric) concept1).getHiNormal(); criticalLow = ((ConceptNumeric) concept1).getLowCritical(); criticalHigh = ((ConceptNumeric) concept1).getHiCritical(); // Only get observations2 if both concepts share the same units; update chart title and ranges if (concept2 != null) { String concept2Units = ((ConceptNumeric) concept2).getUnits(); if (concept2Units != null && concept2Units.equals(rangeAxisTitle)) { observations2 = Context.getObsService().getObservationsByPersonAndConcept(patient, concept2); chartTitle += " + " + concept2.getBestName(request.getLocale()).getName(); if (((ConceptNumeric) concept2).getHiAbsolute() != null && ((ConceptNumeric) concept2).getHiAbsolute() > maxRange) maxRange = ((ConceptNumeric) concept2).getHiAbsolute(); if (((ConceptNumeric) concept2).getLowAbsolute() != null && ((ConceptNumeric) concept2).getLowAbsolute() < minRange) minRange = ((ConceptNumeric) concept2).getLowAbsolute(); } else { log.warn("Units for concept id: " + conceptId2 + " don't match units for concept id: " + conceptId1 + ". Only displaying " + conceptId1); concept2 = null; // nullify concept2 so that the legend isn't shown later } } } else { chartTitle = "Concept " + conceptId1 + " not found"; rangeAxisTitle = "Value"; } // Overwrite with user-specified values, otherwise use default values if (units != null && units.length() > 0) rangeAxisTitle = units; if (minRangeString != null) { minRange = Double.parseDouble(minRangeString); userSpecifiedMinRange = true; } if (maxRangeString != null) { maxRange = Double.parseDouble(maxRangeString); userSpecifiedMaxRange = true; } if (chartTitle == null) chartTitle = ""; if (rangeAxisTitle == null) rangeAxisTitle = ""; if (seriesTitle1 == null) seriesTitle1 = chartTitle; if (seriesTitle2 == null) seriesTitle2 = chartTitle; if (minRange == null) minRange = 0.0; if (maxRange == null) maxRange = 200.0; // Create data set TimeSeriesCollection dataset = new TimeSeriesCollection(); TimeSeries series1, series2; // Interval-dependent units Class<? extends RegularTimePeriod> timeScale = null; if (toDate.getTime() - fromDate.getTime() <= 86400000) { // Interval <= 1 day: minutely timeScale = Minute.class; timeAxisTitle = "Time"; } else if (toDate.getTime() - fromDate.getTime() <= 259200000) { // Interval <= 3 days: hourly timeScale = Hour.class; timeAxisTitle = "Time"; } else { timeScale = Day.class; timeAxisTitle = "Date"; } series1 = new TimeSeries(concept1.getBestName(Context.getLocale()).getName(), timeScale); if (concept2 == null) series2 = new TimeSeries("NULL", Hour.class); else series2 = new TimeSeries(concept2.getBestName(Context.getLocale()).getName(), timeScale); // Add data points for concept1 for (Obs obs : observations1) { if (obs.getValueNumeric() != null && obs.getObsDatetime().getTime() >= fromDate.getTime() && obs.getObsDatetime().getTime() < toDate.getTime()) { cal.setTime(obs.getObsDatetime()); if (timeScale == Minute.class) { Minute min = new Minute(cal.get(Calendar.MINUTE), cal.get(Calendar.HOUR_OF_DAY), cal .get(Calendar.DAY_OF_MONTH), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR)); series1.addOrUpdate(min, obs.getValueNumeric()); } else if (timeScale == Hour.class) { Hour hour = new Hour(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.DAY_OF_MONTH), cal .get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR)); series1.addOrUpdate(hour, obs.getValueNumeric()); } else { Day day = new Day(cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR)); series1.addOrUpdate(day, obs.getValueNumeric()); } } } // Add data points for concept2 for (Obs obs : observations2) { if (obs.getValueNumeric() != null && obs.getObsDatetime().getTime() >= fromDate.getTime() && obs.getObsDatetime().getTime() < toDate.getTime()) { cal.setTime(obs.getObsDatetime()); if (timeScale == Minute.class) { Minute min = new Minute(cal.get(Calendar.MINUTE), cal.get(Calendar.HOUR_OF_DAY), cal .get(Calendar.DAY_OF_MONTH), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR)); series2.addOrUpdate(min, obs.getValueNumeric()); } else if (timeScale == Hour.class) { Hour hour = new Hour(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.DAY_OF_MONTH), cal .get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR)); series2.addOrUpdate(hour, obs.getValueNumeric()); } else { Day day = new Day(cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR)); series2.addOrUpdate(day, obs.getValueNumeric()); } } } // Add series to dataset dataset.addSeries(series1); if (!series2.isEmpty()) dataset.addSeries(series2); // As of JFreeChart 1.0.11 the default background color is dark grey instead of white. // This line restores the original white background. ChartFactory.setChartTheme(StandardChartTheme.createLegacyTheme()); JFreeChart chart = null; // Show legend only if more than one series if (concept2 == null) chart = ChartFactory.createTimeSeriesChart(chartTitle, timeAxisTitle, rangeAxisTitle, dataset, false, false, false); else chart = ChartFactory.createTimeSeriesChart(chartTitle, timeAxisTitle, rangeAxisTitle, dataset, true, false, false); // Customize title font Font font = new Font("Arial", Font.BOLD, 12); TextTitle title = chart.getTitle(); title.setFont(font); chart.setTitle(title); // Add subtitle, unless 'hideDate' has been passed if (hideDate == null) { TextTitle subtitle = new TextTitle(fromDate.toString() + " - " + toDate.toString()); subtitle.setFont(font); chart.addSubtitle(subtitle); } XYPlot plot = (XYPlot) chart.getPlot(); plot.setNoDataMessage("No Data Available"); // Add abnormal/critical range background color (only for single-concept graphs) if (concept2 == null) { IntervalMarker abnormalLow, abnormalHigh, critical; if (normalHigh != null) { abnormalHigh = new IntervalMarker(normalHigh, maxRange, COLOR_ABNORMAL); plot.addRangeMarker(abnormalHigh); } if (normalLow != null) { abnormalLow = new IntervalMarker(minRange, normalLow, COLOR_ABNORMAL); plot.addRangeMarker(abnormalLow); } if (criticalHigh != null) { critical = new IntervalMarker(criticalHigh, maxRange, COLOR_CRITICAL); plot.addRangeMarker(critical); } if (criticalLow != null) { critical = new IntervalMarker(minRange, criticalLow, COLOR_CRITICAL); plot.addRangeMarker(critical); } // there is data outside of the absolute lower limits for this concept (or of what the user specified as minrange) if (plot.getRangeAxis().getLowerBound() < minRange) { IntervalMarker error = new IntervalMarker(plot.getRangeAxis().getLowerBound(), minRange, COLOR_ERROR); plot.addRangeMarker(error); } if (plot.getRangeAxis().getUpperBound() > maxRange) { IntervalMarker error = new IntervalMarker(maxRange, plot.getRangeAxis().getUpperBound(), COLOR_ERROR); plot.addRangeMarker(error); } } // Visuals XYItemRenderer r = plot.getRenderer(); if (r instanceof XYLineAndShapeRenderer) { XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) r; renderer.setBaseShapesFilled(true); renderer.setBaseShapesVisible(true); } // Customize the plot (range and domain axes) // Modify x-axis (datetime) DateAxis timeAxis = (DateAxis) plot.getDomainAxis(); if (timeScale == Day.class) timeAxis.setDateFormatOverride(new SimpleDateFormat("dd-MMM-yyyy")); timeAxis.setRange(fromDate, toDate); // Set y-axis range (values) NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); if (userSpecifiedMinRange) minRange = (rangeAxis.getLowerBound() < minRange) ? rangeAxis.getLowerBound() : minRange; if (userSpecifiedMaxRange) // otherwise we just use default range maxRange = (rangeAxis.getUpperBound() > maxRange) ? rangeAxis.getUpperBound() : maxRange; rangeAxis.setRange(minRange, maxRange); return chart; } /** * Get the FromDate object from the given string that is the time in milliseconds. If * dateFromRequest is null, return 1 year ago from today. * * @param dateFromRequest String that was passed into this servlet * @return Date parsed from dateFromRequest string * @should return one year previous to today if parameter is null * @should return same date as given string parameter */ protected Date getFromDate(String dateFromRequest) { Date returnedDate = new Date(); // default to right now if (dateFromRequest != null && dateFromRequest.length() > 0) returnedDate.setTime(Long.parseLong(dateFromRequest)); else { Calendar cal = Calendar.getInstance(); cal.setTime(returnedDate); cal.set(cal.get(Calendar.YEAR) - 1, cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0); returnedDate = cal.getTime(); } return returnedDate; } /** * Get the toDate object from the given string that is the time in milliseconds. If * dateFromRequest is null, return tomorrow's date. * * @param dateFromRequest String that was passed into this servlet * @return Date parsed from dateFromRequest string * @should return next months date if parameter is null * @should return date one day after given string date * @should set hour minute and second to zero */ protected Date getToDate(String dateFromRequest) { Calendar cal = Calendar.getInstance(); Date toDate = new Date(); if (dateFromRequest != null && dateFromRequest.length() > 0) cal.setTimeInMillis(Long.parseLong(dateFromRequest)); else cal.setTime(toDate); // set +1 day so the selected toDate is fully included in the interval cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH) + 1, 0, 0, 0); toDate = cal.getTime(); return toDate; } /** * There are no post actions. Ignore this method. */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }