Java tutorial
/************************************************************************************************** * Fire History Analysis and Exploration System (FHAES), Copyright (C) 2015 * * Contributors: Aaron Decker, Michael Ababio, Zachariah Ferree, Matthew Willie, Peter Brewer * * This program is free software: you can redistribute it and/or modify it under the terms of * the GNU General Public License as published by the Free Software Foundation, either version * 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. * If not, see <http://www.gnu.org/licenses/>. * *************************************************************************************************/ package org.fhaes.neofhchart.svg; import java.awt.Color; import java.awt.Font; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Scanner; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.batik.bridge.BridgeException; import org.apache.batik.dom.svg.SVGDOMImplementation; import org.apache.batik.dom.svg.SVGLocatableSupport; import org.apache.commons.lang.StringUtils; import org.fhaes.enums.AnnoteMode; import org.fhaes.enums.EventTypeToProcess; import org.fhaes.enums.FeedbackDisplayProtocol; import org.fhaes.enums.FeedbackMessageType; import org.fhaes.enums.FireFilterType; import org.fhaes.enums.LabelOrientation; import org.fhaes.enums.SampleDepthFilterType; import org.fhaes.feedback.FeedbackPreferenceManager.FeedbackDictionary; import org.fhaes.fhfilereader.AbstractFireHistoryReader; import org.fhaes.fhfilereader.FHFile; import org.fhaes.gui.MainWindow; import org.fhaes.model.FHCategoryEntry; import org.fhaes.model.FHSeries; import org.fhaes.neofhchart.ChartActions.SeriesSortType; import org.fhaes.neofhchart.FHSeriesSVG; import org.fhaes.preferences.App; import org.fhaes.preferences.FHAESPreferences.PrefKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.w3c.dom.svg.SVGRect; /** * FireChartSVG Class. Graphs a fire history chart as an SVG using FHUtil's AbstractFireHistoryReader. * * @author Aaron Decker, Michael Ababio, Zachariah Ferree, Matthew Willie, Peter Brewer */ public class FireChartSVG { // Declare logger private static final Logger log = LoggerFactory.getLogger(FireChartSVG.class); // Declare DOMImplementation (this is the Document Object Model API which is used for creating SVG documents) private DOMImplementation impl = SVGDOMImplementation.getDOMImplementation(); // Declare SVG document namespace (this is what tells the API that we are creating an SVG document) private String svgNS = SVGDOMImplementation.SVG_NAMESPACE_URI; // Declare SVG document instance (this is the actual SVG document) private Document doc = impl.createDocument(svgNS, "svg", null); // Declare protected constants protected static final int SERIES_HEIGHT = 10; // Declare local constants private static final int CATEGORY_PADDING_AMOUNT = 40; // Declare local variables private AbstractFireHistoryReader reader; private int chartXOffset = 50; private int chartWidth = 1000; private int categoryGroupPadding = 0; private int widestChronologyLabelSize = 0; private boolean showFires = true; private boolean showInjuries = true; private boolean showPith = true; private boolean showBark = true; private boolean showInnerRing = true; private boolean showOuterRing = true; public boolean traditionalData = false; private AnnoteMode annotemode = AnnoteMode.NONE; private SeriesSortType lastTypeSortedBy = SeriesSortType.NAME; private ArrayList<FHSeriesSVG> seriesSVGList = new ArrayList<FHSeriesSVG>(); // Declare builder objects private final CompositePlotElementBuilder compositePlotEB; private final LegendElementBuilder legendEB; private final PercentScarredPlotElementBuilder percentScarredPlotEB; private final SampleRecorderPlotElementBuilder sampleRecorderPlotEB; private final SeriesElementBuilder seriesEB; private final TimeAxisElementBuilder timeAxisEB; // Java <-> ECMAScript interop used for message passing with ECMAScript. Note not thread-safe private static int chartCounter = 0; private static int lineGensym = 0; // only used in drawRect -- I just need a unique id private static Map<Integer, FireChartSVG> chart_map; private int totalHeight = 0; private int chartNum; /** * The constructor builds the DOM of the SVG. * * @param f */ public FireChartSVG(AbstractFireHistoryReader f) { // Initialize the builder objects compositePlotEB = new CompositePlotElementBuilder(this); legendEB = new LegendElementBuilder(this); percentScarredPlotEB = new PercentScarredPlotElementBuilder(this); sampleRecorderPlotEB = new SampleRecorderPlotElementBuilder(this); seriesEB = new SeriesElementBuilder(this); timeAxisEB = new TimeAxisElementBuilder(this); // Assign number for message passing from ECMAscript chartNum = chartCounter; chartCounter++; if (chart_map == null) { chart_map = new HashMap<Integer, FireChartSVG>(); } chart_map.put(chartNum, this); reader = f; ArrayList<FHSeriesSVG> seriesToAdd = FireChartUtil.seriesListToSeriesSVGList(f.getSeriesList()); if (!seriesSVGList.isEmpty()) { seriesSVGList.clear(); } for (int i = 0; i < seriesToAdd.size(); i++) { try { FHSeries currentSeries = seriesToAdd.get(i); // Add the default category entry if the current series has no defined entries (this is necessary for category groupings) if (currentSeries.getCategoryEntries().isEmpty()) { currentSeries.getCategoryEntries() .add(new FHCategoryEntry(currentSeries.getTitle(), "default", "default")); } seriesSVGList.add(new FHSeriesSVG(seriesToAdd.get(i), i)); } catch (Exception e) { e.printStackTrace(); } } Element svgRoot = doc.getDocumentElement(); // Set up the scripts for Java / ECMAScript interop Element script = doc.createElementNS(svgNS, "script"); script.setAttributeNS(null, "type", "text/ecmascript"); try { // File script_file = new File("./script.js"); ClassLoader cl = org.fhaes.neofhchart.svg.FireChartSVG.class.getClassLoader(); Scanner scanner = new Scanner(cl.getResourceAsStream("script.js")); String script_string = ""; while (scanner.hasNextLine()) { script_string += scanner.nextLine(); } script_string += ("var chart_num = " + chartNum + ";"); Text script_text = doc.createTextNode(script_string); script.appendChild(script_text); scanner.close(); } catch (Exception e) { e.printStackTrace(); } svgRoot.appendChild(script); // The padding_grouper is used to add in some padding around the chart as a whole Element padding_grouper = doc.createElementNS(svgNS, "g"); padding_grouper.setAttributeNS(null, "id", "padding_g"); padding_grouper.setAttributeNS(null, "transform", "translate (" + chartXOffset + ",20)"); svgRoot.appendChild(padding_grouper); // Build grouper to hold annotation elements Element annote_g = doc.createElementNS(svgNS, "g"); annote_g.setAttributeNS(null, "id", "annote_g"); padding_grouper.appendChild(annote_g); // Build chart title Element chart_title_g = doc.createElementNS(svgNS, "g"); chart_title_g.setAttributeNS(null, "id", "chart_title_g"); padding_grouper.appendChild(chart_title_g); // Build the time axis Element time_axis_g = doc.createElementNS(svgNS, "g"); time_axis_g.setAttributeNS(null, "id", "time_axis_g"); padding_grouper.appendChild(time_axis_g); // Build index plot Element index_plot_g = doc.createElementNS(svgNS, "g"); index_plot_g.setAttributeNS(null, "id", "index_plot_g"); padding_grouper.appendChild(index_plot_g); // Build chronology plot Element chrono_plot_g = doc.createElementNS(svgNS, "g"); chrono_plot_g.setAttributeNS(null, "id", "chrono_plot_g"); padding_grouper.appendChild(chrono_plot_g); // Build composite plot Element comp_plot_g = doc.createElementNS(svgNS, "g"); comp_plot_g.setAttributeNS(null, "id", "comp_plot_g"); padding_grouper.appendChild(comp_plot_g); // Build legend Element legend_g = doc.createElementNS(svgNS, "g"); legend_g.setAttributeNS(null, "id", "legend_g"); padding_grouper.appendChild(legend_g); // Finish up the initialization buildElements(); positionSeriesLines(); positionChartGroupersAndDrawTimeAxis(); sortSeriesAccordingToPreference(); }; /** * Gets the SVG document instance for this chart. * * @return */ public Document getSVGDocument() { return doc; } /** * Gets the abstract fire history reader for this chart. * * @return */ public AbstractFireHistoryReader getReader() { return reader; } /** * Get the name of the file being read. * * @return */ public String getName() { return reader.getName(); } /** * Gets the chart number for use in the ECMAscript. * * @return */ public int getChartNum() { return chartNum; } /** * Get the FireChartSVG with the specified ID. * * @param id * @return */ public static FireChartSVG getChart(Integer id) { return chart_map.get(id); } /** * This function returns the up-to-date list of series. * * @return the current list of series */ public ArrayList<FHSeriesSVG> getCurrentSeriesList() { return seriesSVGList; } /** * Save the current SVG to the specified file. * * @param f */ public void saveSVGToDisk(File f) { if (!f.getAbsolutePath().toLowerCase().endsWith("svg")) { f = new File(f.getAbsolutePath() + ".svg"); } try { if (!f.exists()) { f.createNewFile(); } FileOutputStream fstream = new FileOutputStream(f); printDocument(doc, fstream); MainWindow.getInstance().getFeedbackMessagePanel().updateFeedbackMessage(FeedbackMessageType.INFO, FeedbackDisplayProtocol.AUTO_HIDE, FeedbackDictionary.NEOFHCHART_SVG_EXPORT_MESSAGE.toString()); } catch (Exception e) { e.printStackTrace(); } } /** * Helper function that deletes all child tags of the specified element. * * @param e */ private void deleteAllChildren(Element e) { if (e != null) { NodeList n = e.getChildNodes(); for (int i = 0; i < n.getLength(); i++) { e.removeChild(n.item(i)); } } } /** * Get the first year in the chart. Will be the first year in the file, or the year specified by the user if different. * * @return */ public int getFirstChartYear() { if (App.prefs.getBooleanPref(PrefKey.CHART_AXIS_X_AUTO_RANGE, true)) { return applyBCYearOffset(reader.getFirstYear()); } else { return applyBCYearOffset(App.prefs.getIntPref(PrefKey.CHART_AXIS_X_MIN, reader.getFirstYear())); } } /** * Get the last year in the chart. Will be the last year in the file, or the year specified by the user if different. * * @return */ public int getLastChartYear() { if (App.prefs.getBooleanPref(PrefKey.CHART_AXIS_X_AUTO_RANGE, true)) { return applyBCYearOffset(reader.getLastYear()); } else { return applyBCYearOffset(App.prefs.getIntPref(PrefKey.CHART_AXIS_X_MAX, reader.getLastYear())); } } /** * Returns the magnitude of the year if it is negative, otherwise returns an offset of one. This is effectively the offset which the * chart must apply while rendering series in order to maintain compatibility with BC years. * * @param year * @return */ private int applyBCYearOffset(int originalYear) { int effectiveFirstYear = getEffectiveFirstYear(); // Apply the offset transformation if (effectiveFirstYear < 0 && originalYear < 0) { return originalYear + Math.abs(effectiveFirstYear); } else if (effectiveFirstYear < 0 && originalYear >= 0) { return originalYear + Math.abs(effectiveFirstYear) + 1; } else { return originalYear; } } /** * Performs the inverse operation of applyBCYearOffset. * * @param year * @return */ private int removeBCYearOffset(int offsetYear) { int effectiveFirstYear = getEffectiveFirstYear(); // Remove the offset transformation if (effectiveFirstYear < 0 && offsetYear - Math.abs(effectiveFirstYear) < 0) { return offsetYear - Math.abs(effectiveFirstYear); } else if (effectiveFirstYear < 0 && offsetYear - Math.abs(effectiveFirstYear) >= 0) { return offsetYear - Math.abs(effectiveFirstYear) - 1; } else { return offsetYear; } } /** * Determines what year to use as the effective first year when applying or removing the BC offset. * * @return the effective first year */ private int getEffectiveFirstYear() { // Determine what to reference as the first year during the transformation if (App.prefs.getBooleanPref(PrefKey.CHART_AXIS_X_AUTO_RANGE, true)) { return reader.getFirstYear(); } else { return App.prefs.getIntPref(PrefKey.CHART_AXIS_X_MIN, reader.getFirstYear()); } } /** * Gets the base width of this chart. * * @return chartWidth */ public int getChartWidth() { return chartWidth; } /** * Gets the total width of this chart. * * @return the chartWidth plus the padding due to chronology label size */ public int getTotalWidth() { return chartWidth + this.widestChronologyLabelSize + 300; } /** * Gets the total height of this chart. * * @return totalHeight */ public int getTotalHeight() { return totalHeight; } /** * This method swaps the selected series with the series above it. * * @param series_name: Name of the series to move up */ public void moveSeriesUp(String series_name) { int i = 0; for (i = 0; i < seriesSVGList.size() && !seriesSVGList.get(i).getTitle().equals(series_name); i++) { ; // loop until the index of the desired series is found } if (i > 0) { do { FHSeriesSVG series = seriesSVGList.get(i); seriesSVGList.set(i, seriesSVGList.get(i - 1)); try { seriesSVGList.set(i - 1, new FHSeriesSVG(series, series.getSequenceInFile())); } catch (Exception e) { e.printStackTrace(); } i--; positionSeriesLines(); } while (i > 0 && !seriesSVGList.get(i + 1).isVisible()); } } /** * This method swaps the selected series with the series below it. * * @param series_name: Name of the series to move down */ public void moveSeriesDown(String series_name) { int i = 0; for (i = 0; i < seriesSVGList.size() && !seriesSVGList.get(i).getTitle().equals(series_name); i++) { ; // loop until the index of the desired series is found } if (i < seriesSVGList.size() - 1) { do { FHSeriesSVG series = seriesSVGList.get(i); seriesSVGList.set(i, seriesSVGList.get(i + 1)); try { seriesSVGList.set(i + 1, new FHSeriesSVG(series, series.getSequenceInFile())); } catch (Exception e) { e.printStackTrace(); } i++; positionSeriesLines(); } while (i < seriesSVGList.size() - 1 && !seriesSVGList.get(i - 1).isVisible()); } } /** * Handles the positioning of the series lines on the chart. */ private void positionSeriesLines() { int series_spacing_and_height = App.prefs.getIntPref(PrefKey.CHART_CHRONOLOGY_PLOT_SPACING, 5) + SERIES_HEIGHT; int hidden = 0; // Reset the amount of padding necessary for category groupings categoryGroupPadding = 0; // Define a string for keeping track of the category groups ArrayList<String> categoryGroupsProcessed = new ArrayList<String>(); for (int i = 0; i < seriesSVGList.size(); i++) { FHSeries seriesSVG = seriesSVGList.get(i); Element series_group = doc.getElementById("series_group_" + seriesSVG.getTitle()); String visibility_string = seriesSVGList.get(i).isVisible() ? "inline" : "none"; if (seriesSVGList.get(i).isVisible()) { // Inject the category group spacing and label text as different category groups are positioned if (lastTypeSortedBy == SeriesSortType.CATEGORY && App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CATEGORY_GROUPS, true)) { String currentCategoryGroup = seriesSVG.getCategoryEntries().get(0).getContent(); if (!categoryGroupsProcessed.contains(currentCategoryGroup)) { // Keep track of which category groups have already been processed categoryGroupsProcessed.add(currentCategoryGroup); Element label_text_g = doc.createElementNS(svgNS, "g"); label_text_g.setAttributeNS(null, "transform", "translate(0," + Integer.toString(-(CATEGORY_PADDING_AMOUNT / 2)) + ")"); // Apply the label coloring as necessary if (App.prefs.getBooleanPref(PrefKey.CHART_AUTOMATICALLY_COLORIZE_LABELS, false)) { Color labelColor = FireChartUtil.pickColorFromInteger(categoryGroupsProcessed.size()); label_text_g.appendChild( seriesEB.getCategoryLabelTextElement(currentCategoryGroup, labelColor)); } else { label_text_g.appendChild( seriesEB.getCategoryLabelTextElement(currentCategoryGroup, Color.BLACK)); } series_group.appendChild(label_text_g); // Handle the padding of category groups depending on whether the label is shown if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CATEGORY_LABELS, true)) { label_text_g.setAttributeNS(null, "display", "inline"); categoryGroupPadding += CATEGORY_PADDING_AMOUNT; } else { label_text_g.setAttributeNS(null, "display", "none"); categoryGroupPadding += CATEGORY_PADDING_AMOUNT / 2; } } } series_group.setAttributeNS(null, "transform", "translate(0," + Integer.toString(((i - hidden) * series_spacing_and_height) + categoryGroupPadding) + ")"); } else { hidden++; } series_group.setAttributeNS(null, "display", visibility_string); } } /** * Positions the various parts of the fire chart. This method also re-creates the time axis since it is dependent on the total height of * the svg, due to the dashed tick lines that run vertically to denote years. */ public void positionChartGroupersAndDrawTimeAxis() { // Calculate plot dimensions int cur_bottom = 0; // used for tracking where the bottom of the chart is as it is being built int index_plot_height = App.prefs.getIntPref(PrefKey.CHART_INDEX_PLOT_HEIGHT, 100); int series_spacing_and_height = App.prefs.getIntPref(PrefKey.CHART_CHRONOLOGY_PLOT_SPACING, 5) + SERIES_HEIGHT; if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHART_TITLE, true)) { cur_bottom += App.prefs.getIntPref(PrefKey.CHART_TITLE_FONT_SIZE, 20) + 10; } if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_INDEX_PLOT, true)) { cur_bottom += index_plot_height + series_spacing_and_height; } int chronology_plot_y = cur_bottom; int num_visible = 0; for (int i = 0; i < seriesSVGList.size(); i++) { if (seriesSVGList.get(i).isVisible()) { num_visible++; } } int chronology_plot_height = num_visible * series_spacing_and_height + SERIES_HEIGHT; if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHRONOLOGY_PLOT, true)) { cur_bottom += chronology_plot_height + series_spacing_and_height + categoryGroupPadding; } int composite_plot_y = cur_bottom; int composite_plot_height = App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_HEIGHT, 70); if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_COMPOSITE_PLOT, true)) { cur_bottom += composite_plot_height + series_spacing_and_height; } int total_height = cur_bottom + series_spacing_and_height; // reset svg dimensions Element svgRoot = doc.getDocumentElement(); // build time axis Element time_axis_g = doc.getElementById("time_axis_g"); // delete everything in the current time axis NodeList n = time_axis_g.getChildNodes(); // because getChildNodes doesn't return a seq for (int i = 0; i < n.getLength(); i++) { // no, instead we get a non-iterable custom data-structure :( time_axis_g.removeChild(n.item(i)); } // add in the new time axis time_axis_g.appendChild(getTimeAxis(total_height)); // set the translations for the chronology plot grouper Element chrono_plot_g = doc.getElementById("chrono_plot_g"); chrono_plot_g.setAttributeNS(null, "transform", "translate(0," + chronology_plot_y + ")"); // set the translations for the composite plot grouper Element comp_plot_g = doc.getElementById("comp_plot_g"); comp_plot_g.setAttributeNS(null, "transform", "translate(0," + composite_plot_y + ")"); // move the legend Element legend_g = doc.getElementById("legend_g"); legend_g.setAttributeNS(null, "transform", "translate(" + (chartWidth + 10 + this.widestChronologyLabelSize + 50) + ", " + 0 + ")"); // set the annote canvas dimensions (so it can catch key bindings) Element annote_canvas = doc.getElementById("annote_canvas"); annote_canvas.setAttributeNS(null, "width", Integer.toString(chartWidth)); annote_canvas.setAttributeNS(null, "height", Integer.toString(total_height)); // set document dimensions for png and pdf export // svgRoot.setAttributeNS(null, "width", (chart_width + this.widestChronologyLabelSize + 150 + 350) + "px"); svgRoot.setAttributeNS(null, "width", getTotalWidth() + "px"); int root_height = (total_height + 50 > 400) ? total_height + 50 : 400; totalHeight = root_height; svgRoot.setAttributeNS(null, "height", root_height + "px"); } /** * Clear out the groupers and build the chart components. */ public void buildElements() { // Rebuild the annotation canvas Element annote_g = doc.getElementById("annote_g"); deleteAllChildren(annote_g); Element canvas = getAnnoteCanvas(); if (canvas != null) { annote_g.appendChild(canvas); } // Rebuild the chart title Element chart_title_g = doc.getElementById("chart_title_g"); deleteAllChildren(chart_title_g); if (App.prefs.getBooleanPref(PrefKey.CHART_TITLE_USE_DEFAULT_NAME, true)) { FHFile currentFile = getReader().getFHFile(); if (currentFile.getSiteName().length() > 0) { chart_title_g.appendChild(getChartTitle(currentFile.getSiteName())); } else { chart_title_g.appendChild(getChartTitle(currentFile.getFileNameWithoutExtension())); } } else { chart_title_g.appendChild( getChartTitle(App.prefs.getPref(PrefKey.CHART_TITLE_OVERRIDE_VALUE, "Fire Chart"))); } if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHART_TITLE, true)) { chart_title_g.setAttributeNS(null, "display", "inline"); } else { chart_title_g.setAttributeNS(null, "display", "none"); } // Rebuild the index plot Element index_plot_g = doc.getElementById("index_plot_g"); deleteAllChildren(index_plot_g); index_plot_g.appendChild(getIndexPlot()); sortSeriesAccordingToPreference(); // Rebuild the chronology plot rebuildChronologyPlot(); // Rebuild the composite plot Element comp_plot_g = doc.getElementById("comp_plot_g"); deleteAllChildren(comp_plot_g); comp_plot_g.appendChild(getCompositePlot()); // Rebuild the legend Element legend_g = doc.getElementById("legend_g"); deleteAllChildren(legend_g); legend_g.appendChild(getLegend()); positionChartGroupersAndDrawTimeAxis(); } /** * Builds a series line based of the input seriesSVG object. * * @param seriesSVG * @return a series line element */ private Element buildSingleSeriesLine(FHSeriesSVG seriesSVG) { Element series_group = doc.createElementNS(svgNS, "g"); series_group.setAttributeNS(null, "id", seriesSVG.getTitle()); // draw in the recording and non-recording lines Element line_group = doc.createElementNS(svgNS, "g"); boolean[] recording_years = seriesSVG.getRecordingYears(); int begin_index = 0; int last_index = recording_years.length - 1; if (recording_years.length != 0) { if (applyBCYearOffset(seriesSVG.getFirstYear()) < this.getFirstChartYear()) { // User has trimmed the start of this data series off int firstyear = applyBCYearOffset(seriesSVG.getFirstYear()); int thisfirstyear = this.getFirstChartYear(); begin_index = thisfirstyear - firstyear; } if (applyBCYearOffset(seriesSVG.getLastYear()) > this.getLastChartYear()) { // User has trimmed the end of this data series off int recleng = recording_years.length; int lastyear = applyBCYearOffset(seriesSVG.getLastYear()); int thislastyear = this.getLastChartYear(); last_index = recleng - (lastyear - thislastyear) - 1; } boolean isRecording = recording_years[0]; for (int j = 0; j <= last_index; j++) { if (isRecording != recording_years[j] || j == last_index) { // Need to draw a line line_group.appendChild( seriesEB.getSeriesLine(isRecording, begin_index, j, seriesSVG.getLineColor())); begin_index = j; isRecording = recording_years[j]; } } } series_group.appendChild(line_group); // add in fire events if (showFires) { Element series_fire_events = doc.createElementNS(svgNS, "g"); boolean[] fire_years = seriesSVG.getEventYears(); for (int j = 0; j < fire_years.length; j++) { if (fire_years[j] && j <= last_index) { String translate = "translate(" + Double.toString(j - FireChartUtil.pixelsToYears(0.5, chartWidth, getFirstChartYear(), getLastChartYear())) + "," + Integer.toString(-SERIES_HEIGHT / 2) + ")"; String scale = "scale(" + FireChartUtil.pixelsToYears(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"; Element fire_event_g = doc.createElementNS(svgNS, "g"); fire_event_g.setAttributeNS(null, "class", "fire_marker"); fire_event_g.setAttributeNS(null, "stroke", FireChartUtil.colorToHexString(seriesSVG.getLineColor())); fire_event_g.setAttributeNS(null, "transform", translate + scale); fire_event_g.appendChild(seriesEB.getFireYearMarker(seriesSVG.getLineColor())); series_fire_events.appendChild(fire_event_g); } } series_group.appendChild(series_fire_events); } // add in injury events if (showInjuries) { Element series_injury_events = doc.createElementNS(svgNS, "g"); boolean[] injury_years = seriesSVG.getInjuryYears(); for (int j = 0; j < injury_years.length; j++) { if (injury_years[j] && j <= last_index) { String translate = "translate(" + Double.toString(j - FireChartUtil.pixelsToYears(1.5, chartWidth, getFirstChartYear(), getLastChartYear())) + "," + Integer.toString(-SERIES_HEIGHT / 2) + ")"; String scale = "scale(" + FireChartUtil.pixelsToYears(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"; Element injury_event_g = doc.createElementNS(svgNS, "g"); injury_event_g.setAttributeNS(null, "class", "injury_marker"); injury_event_g.setAttributeNS(null, "stroke", FireChartUtil.colorToHexString(seriesSVG.getLineColor())); injury_event_g.setAttributeNS(null, "transform", translate + scale); injury_event_g.appendChild(seriesEB.getInjuryYearMarker(3, seriesSVG.getLineColor())); series_injury_events.appendChild(injury_event_g); } } series_group.appendChild(series_injury_events); } // add in inner year pith marker if (showPith && seriesSVG.hasPith() || showInnerRing && !seriesSVG.hasPith()) { if (applyBCYearOffset(seriesSVG.getFirstYear()) >= getFirstChartYear()) { // no translation because the inner year is at year=0 String translate = "translate(" + (0 - FireChartUtil.pixelsToYears(0.5, chartWidth, getFirstChartYear(), getLastChartYear())) + ",0)"; String scale = "scale(" + FireChartUtil.pixelsToYears(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"; Element pith_marker_g = doc.createElementNS(svgNS, "g"); pith_marker_g.setAttributeNS(null, "transform", translate + scale); pith_marker_g.appendChild( seriesEB.getInnerYearPithMarker(seriesSVG.hasPith(), 5, seriesSVG.getLineColor())); series_group.appendChild(pith_marker_g); } } // add in outer year bark marker if ((showBark && seriesSVG.hasBark()) || (showOuterRing && !seriesSVG.hasBark())) { if (applyBCYearOffset(seriesSVG.getLastYear()) <= this.getLastChartYear()) { String translate = "translate(" + (applyBCYearOffset(seriesSVG.getLastYear()) - applyBCYearOffset(seriesSVG.getFirstYear())) + ",0)"; String scale = "scale(" + FireChartUtil.pixelsToYears(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"; Element bark_marker_g = doc.createElementNS(svgNS, "g"); bark_marker_g.setAttribute("transform", translate + scale); bark_marker_g.appendChild( seriesEB.getOuterYearBarkMarker(seriesSVG.hasBark(), 5, seriesSVG.getLineColor())); series_group.appendChild(bark_marker_g); } } return series_group; } /** * Returns a chart title element containing the input text. * * @param chartTitle * @return chartTitleElement */ private Element getChartTitle(String chartTitle) { Text chartTitleText = doc.createTextNode(chartTitle); Integer chartTitleFontSize = App.prefs.getIntPref(PrefKey.CHART_TITLE_FONT_SIZE, 20); Element chartTitleElement = doc.createElementNS(svgNS, "text"); chartTitleElement.setAttributeNS(null, "x", "0"); chartTitleElement.setAttributeNS(null, "y", "0"); chartTitleElement.setAttributeNS(null, "font-family", App.prefs.getPref(PrefKey.CHART_FONT_FAMILY, "Verdana")); chartTitleElement.setAttributeNS(null, "font-size", chartTitleFontSize.toString()); chartTitleElement.appendChild(chartTitleText); return chartTitleElement; } /** * Gets the index plot as an element. * * @return indexPlot */ private Element getIndexPlot() { int indexPlotOffsetAmount = 0; if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHART_TITLE, true)) { indexPlotOffsetAmount = App.prefs.getIntPref(PrefKey.CHART_TITLE_FONT_SIZE, 20) + 10; } Element indexPlot = doc.createElementNS(svgNS, "g"); indexPlot.setAttribute("id", "indexplot"); indexPlot.setAttributeNS(null, "transform", "translate(0," + indexPlotOffsetAmount + ")"); indexPlot.appendChild(getSampleOrRecorderDepthsPlot( App.prefs.getBooleanPref(PrefKey.CHART_SHOW_SAMPLE_DEPTH, false), App.prefs.getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_EVENT))); indexPlot.appendChild(getPercentScarredPlot()); return indexPlot; } /** * Gets the chronology plot as an element. * * @return chronologyPlot */ private Element getChronologyPlot() { Element chronologyPlot = doc.createElementNS(svgNS, "g"); chronologyPlot.setAttributeNS(null, "id", "chronology_plot"); chronologyPlot.setAttributeNS(null, "display", "inline"); // Build all of the series ArrayList<Boolean> series_visible = new ArrayList<Boolean>(); this.showPith = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_PITH_SYMBOL, true); this.showBark = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_BARK_SYMBOL, true); this.showFires = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_FIRE_EVENT_SYMBOL, true); this.showInjuries = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_INJURY_SYMBOL, true); this.showInnerRing = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_INNER_RING_SYMBOL, true); this.showOuterRing = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_OUTER_RING_SYMBOL, true); int fontSize = App.prefs.getIntPref(PrefKey.CHART_CHRONOLOGY_PLOT_LABEL_FONT_SIZE, 8); String longestLabel = "A"; for (int i = 0; i < seriesSVGList.size(); i++) { FHSeries series = seriesSVGList.get(i); if (series.getTitle().length() > longestLabel.length()) longestLabel = series.getTitle(); } widestChronologyLabelSize = FireChartUtil.getStringWidth(Font.PLAIN, App.prefs.getIntPref(PrefKey.CHART_CHRONOLOGY_PLOT_LABEL_FONT_SIZE, 10), longestLabel); // Define a string for keeping track of the category groups ArrayList<String> categoryGroupsProcessed = new ArrayList<String>(); for (int i = 0; i < seriesSVGList.size(); i++) { if (lastTypeSortedBy == SeriesSortType.CATEGORY && App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CATEGORY_GROUPS, true)) { String currentCategoryGroup = seriesSVGList.get(i).getCategoryEntries().get(0).getContent(); // Keep track of which category groups have already been processed if (!categoryGroupsProcessed.contains(currentCategoryGroup)) { categoryGroupsProcessed.add(currentCategoryGroup); } // Apply the series coloring as necessary if (App.prefs.getBooleanPref(PrefKey.CHART_AUTOMATICALLY_COLORIZE_SERIES, false)) { seriesSVGList.get(i) .setLabelColor(FireChartUtil.pickColorFromInteger(categoryGroupsProcessed.size())); seriesSVGList.get(i) .setLineColor(FireChartUtil.pickColorFromInteger(categoryGroupsProcessed.size())); } else { seriesSVGList.get(i).setLabelColor(Color.BLACK); seriesSVGList.get(i).setLineColor(Color.BLACK); } } FHSeriesSVG seriesSVG = seriesSVGList.get(i); series_visible.add(true); Element series_group = doc.createElementNS(svgNS, "g"); series_group.setAttributeNS(null, "id", "series_group_" + seriesSVG.getTitle()); // Add in the series group, which has the lines and ticks Element series_line = buildSingleSeriesLine(seriesSVG); series_line.setAttributeNS(null, "id", "series_line_" + seriesSVG.getTitle()); int x_offset = applyBCYearOffset(seriesSVG.getFirstYear()) - getFirstChartYear(); String translate_string = "translate(" + Integer.toString(x_offset) + ",0)"; String scale_string = "scale(" + FireChartUtil.yearsToPixels(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"; series_line.setAttributeNS(null, "transform", scale_string + " " + translate_string); // Add in the label for the series Element series_name = seriesEB.getSeriesNameTextElement(seriesSVG, fontSize); // Add in the up button Element up_button_g = doc.createElementNS(svgNS, "g"); up_button_g.setAttributeNS(null, "id", "up_button" + i); up_button_g.setAttributeNS(null, "class", "no_export"); up_button_g.setAttributeNS(null, "transform", "translate(" + Double.toString(chartWidth + 15 + widestChronologyLabelSize) + ",-2)"); up_button_g.setAttributeNS(null, "onclick", "FireChartSVG.getChart(chart_num).moveSeriesUp(\"" + seriesSVG.getTitle() + "\"); evt.target.setAttribute('opacity', '0.2');"); up_button_g.setAttributeNS(null, "onmouseover", "evt.target.setAttribute('opacity', '1');"); up_button_g.setAttributeNS(null, "onmouseout", "evt.target.setAttribute('opacity', '0.2');"); up_button_g.appendChild(seriesEB.getUpButton()); // Add in the down button Element down_button_g = doc.createElementNS(svgNS, "g"); down_button_g.setAttributeNS(null, "id", "down_button" + i); down_button_g.setAttributeNS(null, "class", "no_export"); down_button_g.setAttributeNS(null, "transform", "translate(" + Double.toString(chartWidth + 10 + widestChronologyLabelSize + 15) + ",-2)"); down_button_g.setAttributeNS(null, "onclick", "FireChartSVG.getChart(chart_num).moveSeriesDown(\"" + seriesSVG.getTitle() + "\"); evt.target.setAttribute('opacity', '0.2');"); down_button_g.setAttributeNS(null, "onmouseover", "evt.target.setAttribute('opacity', '1');"); down_button_g.setAttributeNS(null, "onmouseout", "evt.target.setAttribute('opacity', '0.2');"); down_button_g.appendChild(seriesEB.getDownButton()); // Determine whether to draw the chronology plot labels if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHRONOLOGY_PLOT_LABELS, true)) { if (lastTypeSortedBy == SeriesSortType.CATEGORY && App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CATEGORY_GROUPS, true)) { // Do not show the up/down buttons if grouping series by category up_button_g.setAttributeNS(null, "display", "none"); down_button_g.setAttributeNS(null, "display", "none"); } else { series_name.setAttributeNS(null, "display", "inline"); up_button_g.setAttributeNS(null, "display", "inline"); down_button_g.setAttributeNS(null, "display", "inline"); } } else { series_name.setAttributeNS(null, "display", "none"); up_button_g.setAttributeNS(null, "display", "none"); down_button_g.setAttributeNS(null, "display", "none"); } series_group.appendChild(series_line); series_group.appendChild(series_name); series_group.appendChild(up_button_g); series_group.appendChild(down_button_g); chronologyPlot.appendChild(series_group); } if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHRONOLOGY_PLOT, true)) { chronologyPlot.setAttributeNS(null, "display", "inline"); } else { chronologyPlot.setAttributeNS(null, "display", "none"); } return chronologyPlot; } /** * Get the composite plot as an element. * * @return compositePlot */ private Element getCompositePlot() { boolean useAbbreviatedYears = App.prefs.getBooleanPref(PrefKey.CHART_COMPOSITE_YEAR_LABELS_TWO_DIGIT, false); String textStr = "2099"; if (useAbbreviatedYears) { textStr = "'99"; } int compositeYearLabelFontSize = App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_YEAR_LABEL_FONT_SIZE, 8); int rotateLabelsAngle = App.prefs .getLabelOrientationPref(PrefKey.CHART_COMPOSITE_LABEL_ALIGNMENT, LabelOrientation.HORIZONTAL) .getAngle(); int stringbuffer = App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_YEAR_LABEL_BUFFER, 5); int compositeYearLabelMaxWidth = 0; int compositeYearLabelHeight = 0; if (rotateLabelsAngle == 0) { compositeYearLabelMaxWidth = FireChartUtil.getStringWidth(Font.PLAIN, compositeYearLabelFontSize, textStr) + stringbuffer; compositeYearLabelHeight = FireChartUtil.getStringHeight(Font.PLAIN, compositeYearLabelFontSize, textStr); } else if (rotateLabelsAngle == 315) { int width = FireChartUtil.getStringWidth(Font.PLAIN, compositeYearLabelFontSize, textStr); double widthsq = width * width; double hyp = Math.sqrt(widthsq + widthsq); compositeYearLabelMaxWidth = (int) ((hyp + stringbuffer)) / 2; compositeYearLabelHeight = (int) hyp; } else { compositeYearLabelMaxWidth = FireChartUtil.getStringHeight(Font.PLAIN, compositeYearLabelFontSize, textStr); compositeYearLabelHeight = FireChartUtil.getStringWidth(Font.PLAIN, compositeYearLabelFontSize, textStr) + stringbuffer; } // compositePlot is centered off of the year 0 A.D. Element compositePlot = doc.createElementNS(svgNS, "g"); compositePlot.setAttributeNS(null, "id", "comp_plot"); compositePlot.setAttributeNS(null, "transform", "scale(" + FireChartUtil.yearsToPixels(chartWidth, getFirstChartYear(), getLastChartYear()) + "," + 1 + ") translate(-" + getFirstChartYear() + ",0) "); // draw the vertical lines for fire years ArrayList<Integer> composite_years = reader.getCompositeFireYears( App.prefs.getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_EVENT), App.prefs.getFireFilterTypePref(PrefKey.CHART_COMPOSITE_FILTER_TYPE, FireFilterType.NUMBER_OF_EVENTS), App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_FILTER_VALUE, 1), App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_MIN_NUM_SAMPLES, 1), App.prefs.getSampleDepthFilterTypePref(PrefKey.CHART_COMPOSITE_SAMPLE_DEPTH_TYPE, SampleDepthFilterType.MIN_NUM_SAMPLES)); // Remove out-of-range years if necessary if (!App.prefs.getBooleanPref(PrefKey.CHART_AXIS_X_AUTO_RANGE, true)) { ArrayList<Integer> composite_years2 = new ArrayList<Integer>(); for (Integer v : composite_years) { if (v > this.getLastChartYear() || v < this.getFirstChartYear()) continue; composite_years2.add(v); } composite_years = composite_years2; } int overlap_margin = (compositeYearLabelMaxWidth); int cur_offset_level = 0; int prev_i = 0; int max_offset_level = 0; // run through the array once to see how many layers we need to keep year labels from overlapping boolean showLabels = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_COMPOSITE_YEAR_LABELS, true); if (showLabels) { for (int i : composite_years) { double pixelsBetweenLabels = FireChartUtil.yearsToPixels(i, chartWidth, getFirstChartYear(), getLastChartYear()) - FireChartUtil.yearsToPixels(prev_i, chartWidth, getFirstChartYear(), getLastChartYear()); if (pixelsBetweenLabels < overlap_margin) { cur_offset_level = cur_offset_level + 1; if (cur_offset_level > max_offset_level) { max_offset_level = cur_offset_level; } } else { cur_offset_level = 0; prev_i = i; } } } else { cur_offset_level = 0; } int num_layers = max_offset_level + 1; // int num_layers = (max_offset_level > 0) ? max_offset_level : 1; // number of layers for putting the offset year labels double chart_height = App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_HEIGHT, 70); if (num_layers > (chart_height / compositeYearLabelFontSize)) { // ensure height is non-negative num_layers = (int) Math.floor(chart_height / compositeYearLabelHeight); } if (showLabels) { if (rotateLabelsAngle == 315) { chart_height = chart_height - (num_layers * (compositeYearLabelHeight / 3)); } else { // height of the composite plot minus the year labels chart_height = chart_height - (num_layers * compositeYearLabelHeight); } } cur_offset_level = 0; prev_i = 0; for (int i : composite_years) { compositePlot.appendChild(compositePlotEB.getEventLine(i, chart_height)); // calculate the offsets for the labels if (FireChartUtil.yearsToPixels(i - prev_i, chartWidth, getFirstChartYear(), getLastChartYear()) < overlap_margin) { cur_offset_level = (cur_offset_level + 1) % num_layers; } else { cur_offset_level = 0; } Element text_g = doc.createElementNS(svgNS, "g"); String scale_str = "scale(" + FireChartUtil.pixelsToYears(chartWidth, getFirstChartYear(), getLastChartYear()) + ", 1)"; if (rotateLabelsAngle == 270) { double offset = chart_height + (cur_offset_level * compositeYearLabelHeight) + compositeYearLabelHeight; String translate_str = "translate(" + (Double.toString(i + (FireChartUtil.pixelsToYears(compositeYearLabelMaxWidth / 2, chartWidth, getFirstChartYear(), getLastChartYear())))) + "," + offset + ")"; text_g.setAttributeNS(null, "transform", translate_str + scale_str + " rotate(" + rotateLabelsAngle + ")"); } else if (rotateLabelsAngle == 315) { double offset = chart_height + (cur_offset_level * (compositeYearLabelHeight / 3)) + compositeYearLabelHeight / 1.3; String translate_str = "translate(" + (Double.toString(i - (FireChartUtil.pixelsToYears(compositeYearLabelMaxWidth / 2, chartWidth, getFirstChartYear(), getLastChartYear())))) + "," + offset + ")"; text_g.setAttributeNS(null, "transform", translate_str + scale_str + " rotate(" + rotateLabelsAngle + ")"); } else { double offset = chart_height + (cur_offset_level * compositeYearLabelHeight) + compositeYearLabelHeight; String translate_str = "translate(" + (Double.toString(i - (FireChartUtil.pixelsToYears(compositeYearLabelMaxWidth / 2, chartWidth, getFirstChartYear(), getLastChartYear())))) + "," + offset + ")"; text_g.setAttributeNS(null, "transform", translate_str + scale_str); } // Grab label string depending on style String year_str = ""; if (useAbbreviatedYears) { year_str = "'"; if (i % 100 < 10) { year_str += "0"; } year_str += Integer.toString(i % 100); } else { year_str = Integer.toString(i); } Text year_text_t = doc.createTextNode(year_str); Element year_text = doc.createElementNS(svgNS, "text"); // year_text.setAttributeNS(null, "x", Integer.toString(i)); // year_text.setAttributeNS(null, "x", "0"); // year_text.setAttributeNS(null, "y", "0"); year_text.setAttributeNS(null, "font-family", App.prefs.getPref(PrefKey.CHART_FONT_FAMILY, "Verdana")); year_text.setAttributeNS(null, "font-size", Integer.toString(compositeYearLabelFontSize)); if (showLabels) { year_text.appendChild(year_text_t); text_g.appendChild(year_text); } compositePlot.appendChild(text_g); prev_i = i; } // Draw a rectangle around it // Needs to be 4 lines to cope with stroke width in different coord sys in x and y compositePlot.appendChild(compositePlotEB.getBorderLine1()); compositePlot.appendChild(compositePlotEB.getBorderLine2(chart_height)); compositePlot.appendChild(compositePlotEB.getBorderLine3(chart_height)); compositePlot.appendChild(compositePlotEB.getBorderLine4(chart_height)); // add the label String translate_string = "translate(" + Double.toString(getLastChartYear() + FireChartUtil.pixelsToYears(10, chartWidth, getFirstChartYear(), getLastChartYear())) + ", " + ((chart_height / 2) + (FireChartUtil.getStringHeight(Font.PLAIN, App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_PLOT_LABEL_FONT_SIZE, 10), App.prefs.getPref(PrefKey.CHART_COMPOSITE_LABEL_TEXT, "Composite"))) / 2) + ")"; String scale_string = "scale(" + FireChartUtil.pixelsToYears(chartWidth, getFirstChartYear(), getLastChartYear()) + ", 1)"; Element comp_name_text_g = doc.createElementNS(svgNS, "g"); comp_name_text_g.setAttributeNS(null, "transform", translate_string + scale_string); comp_name_text_g.appendChild(compositePlotEB.getCompositeLabelTextElement()); compositePlot.appendChild(comp_name_text_g); if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_COMPOSITE_PLOT, true)) { compositePlot.setAttributeNS(null, "display", "inline"); } else { compositePlot.setAttributeNS(null, "display", "none"); } return compositePlot; } /** * This function creates a legend dynamically, based on the current event(s) displayed on the canvas (Fire, Injury, or Fire and Injury). * * @return legend */ private Element getLegend() { int labelWidth = FireChartUtil.getStringWidth(Font.PLAIN, 8, "Outer year without bark") + 40; int labelHeight = FireChartUtil.getStringHeight(Font.PLAIN, 8, "Outer year without bark"); int currentY = 0; // to help position symbols int moveValue = 20; int leftJustified = 20; Element legend = doc.createElementNS(svgNS, "g"); legend.setAttributeNS(null, "id", "legend"); // legend.setAttributeNS(null, "transform", "translate(" + chart_width + ", " + 200 + ")"); // make a "g" for each element, and append to that, so each element is // in a group. then append groups to legend. This is so when the legend moves, // you wont have to manually move all the other elements again. // Get current symbols on canvas and append to legend // RECORDER YEAR Element recorder_g = doc.createElementNS(svgNS, "g"); recorder_g.setAttributeNS(null, "id", "recorder"); recorder_g.appendChild(legendEB.getRecorderYearExample()); Element recorder_desc = legendEB.getDescriptionTextElement("Recorder year", leftJustified, currentY + (labelHeight / 2)); recorder_g.appendChild(recorder_desc); legend.appendChild(recorder_g); // NON-RECORDER YEAR currentY += moveValue; Element nonrecorder_g = doc.createElementNS(svgNS, "g"); nonrecorder_g.appendChild(legendEB.getNonRecorderYearExample(currentY)); Element nonrecorder_desc = legendEB.getDescriptionTextElement("Non-recorder year", leftJustified, currentY + (labelHeight / 2)); nonrecorder_g.appendChild(nonrecorder_desc); legend.appendChild(nonrecorder_g); // currentY += moveValue * 2; // so next symbol is at spot y = 100 if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_FIRE_EVENT_SYMBOL, true)) { // FIRE EVENT MARKER currentY += moveValue; Element fireMarker_g = doc.createElementNS(svgNS, "g"); Element fireMarker = seriesEB.getFireYearMarker(Color.BLACK); fireMarker.setAttributeNS(null, "width", "2"); fireMarker_g.appendChild(fireMarker); fireMarker_g.setAttributeNS(null, "transform", "translate(0, " + currentY + ")"); Element fireMarker_desc = legendEB.getDescriptionTextElement("Fire event", leftJustified, (labelHeight / 2)); fireMarker_g.appendChild(fireMarker_desc); legend.appendChild(fireMarker_g); } if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_INJURY_SYMBOL, true)) { // INJURY EVENT MARKER currentY += moveValue; Element injuryMarker_g = doc.createElementNS(svgNS, "g"); injuryMarker_g.appendChild(seriesEB.getInjuryYearMarker(3, Color.BLACK)); injuryMarker_g.setAttributeNS(null, "transform", "translate(0, " + Integer.toString(currentY) + ")"); Element injuryMarker_desc = legendEB.getDescriptionTextElement("Injury event", leftJustified, (labelHeight / 2)); injuryMarker_g.appendChild(injuryMarker_desc); legend.appendChild(injuryMarker_g); } // PITH WITH NON-RECORDER LINE currentY += moveValue; Element innerPith_g = doc.createElementNS(svgNS, "g"); Element innerPith = seriesEB.getInnerYearPithMarker(true, 5, Color.BLACK); innerPith_g.appendChild(innerPith); innerPith_g.setAttributeNS(null, "transform", "translate(0, " + Integer.toString(currentY) + ")"); Element pithNonrecorder_g = doc.createElementNS(svgNS, "g"); pithNonrecorder_g.appendChild(legendEB.getPithWithNonRecorderLineExample()); innerPith_g.appendChild(pithNonrecorder_g); Element pithNonrecorder_desc = legendEB.getDescriptionTextElement("Inner year with pith", leftJustified, (labelHeight / 2)); innerPith_g.appendChild(pithNonrecorder_desc); legend.appendChild(innerPith_g); // NO PITH WITH NON-RECORDER LINE currentY += moveValue; Element withoutPith_g = doc.createElementNS(svgNS, "g"); Element withoutPith = seriesEB.getInnerYearPithMarker(false, SERIES_HEIGHT, Color.BLACK); withoutPith_g.appendChild(withoutPith); withoutPith_g.setAttributeNS(null, "transform", "translate(0, " + Integer.toString(currentY) + ")"); Element withoutPithNonrecorder_g = doc.createElementNS(svgNS, "g"); withoutPithNonrecorder_g.appendChild(legendEB.getNoPithWithNonRecorderLineExample()); withoutPith_g.appendChild(withoutPithNonrecorder_g); Element withoutPithNonrecorder_desc = legendEB.getDescriptionTextElement("Inner year without pith", leftJustified, (labelHeight / 2)); withoutPith_g.appendChild(withoutPithNonrecorder_desc); legend.appendChild(withoutPith_g); // BARK WITH RECORDER LINE currentY += moveValue; Element withBark_g = doc.createElementNS(svgNS, "g"); Element withBark = seriesEB.getOuterYearBarkMarker(true, 5, Color.BLACK); withBark_g.appendChild(withBark); withBark_g.setAttributeNS(null, "transform", "translate(5, " + Integer.toString(currentY) + ")"); Element barkRecorder_g = doc.createElementNS(svgNS, "g"); barkRecorder_g.appendChild(legendEB.getBarkWithRecorderLineExample()); withBark_g.appendChild(barkRecorder_g); Element barkRecorder_desc = legendEB.getDescriptionTextElement("Outer year with bark", leftJustified - 5, (labelHeight / 2)); withBark_g.appendChild(barkRecorder_desc); legend.appendChild(withBark_g); // NO BARK WITH RECORDER LINE currentY += moveValue; Element withoutBark_g = doc.createElementNS(svgNS, "g"); Element withoutBark = seriesEB.getOuterYearBarkMarker(false, SERIES_HEIGHT, Color.BLACK); withoutBark_g.appendChild(withoutBark); withoutBark_g.setAttributeNS(null, "transform", "translate(5, " + Integer.toString(currentY) + ")"); Element withoutBarkRecorder_g = doc.createElementNS(svgNS, "g"); withoutBarkRecorder_g.appendChild(legendEB.getNoBarkWithRecorderLineExample()); withoutBark_g.appendChild(withoutBarkRecorder_g); Element withoutBarkRecorder_desc = legendEB.getDescriptionTextElement("Outer year without bark", leftJustified - 5, (labelHeight / 2)); withoutBark_g.appendChild(withoutBarkRecorder_desc); legend.appendChild(withoutBark_g); // ADD FILTER DETAILS if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_FILTER_IN_LEGEND, false)) { String s = ""; String longestLabel = s; if (App.prefs.getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_EVENT) .equals(EventTypeToProcess.FIRE_EVENT)) { currentY += moveValue + (labelHeight * 1.3); s = StringUtils.capitalize("Composite based on " + App.prefs .getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_EVENT) .toString().toLowerCase() + " and filtered by:"); Element filterType = legendEB.getDescriptionTextElement(s, leftJustified, (labelHeight / 2)); filterType.setAttributeNS(null, "transform", "translate(-25, " + Integer.toString(currentY) + ")"); legend.appendChild(filterType); longestLabel = s; } else if (App.prefs .getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_AND_INJURY_EVENT) .equals(EventTypeToProcess.FIRE_AND_INJURY_EVENT)) { currentY += moveValue + (labelHeight * 1.3); s = StringUtils.capitalize("Composite based on " + App.prefs .getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_EVENT) .toString().toLowerCase()); Element filterType = legendEB.getDescriptionTextElement(s, leftJustified, (labelHeight / 2)); filterType.setAttributeNS(null, "transform", "translate(-25, " + Integer.toString(currentY) + ")"); legend.appendChild(filterType); currentY += labelHeight * 1.3; filterType = legendEB.getDescriptionTextElement("and filtered by:", leftJustified, (labelHeight / 2)); filterType.setAttributeNS(null, "transform", "translate(-25, " + Integer.toString(currentY) + ")"); legend.appendChild(filterType); longestLabel = s; } else if (App.prefs .getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.INJURY_EVENT) .equals(EventTypeToProcess.INJURY_EVENT)) { currentY += moveValue + (labelHeight * 1.3); s = StringUtils.capitalize("Composite based on " + App.prefs .getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_EVENT) .toString().toLowerCase() + " and filtered by:"); Element filterType = legendEB.getDescriptionTextElement(s, leftJustified, (labelHeight / 2)); filterType.setAttributeNS(null, "transform", "translate(-25, " + Integer.toString(currentY) + ")"); legend.appendChild(filterType); longestLabel = s; } currentY += labelHeight * 1.3; s = " - " + StringUtils.capitalize(App.prefs .getFireFilterTypePref(PrefKey.CHART_COMPOSITE_FILTER_TYPE, FireFilterType.NUMBER_OF_EVENTS) .toString().toLowerCase() + " >= " + App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_FILTER_VALUE, 1)); Element filterType = legendEB.getDescriptionTextElement(s, leftJustified, (labelHeight / 2)); filterType.setAttributeNS(null, "transform", "translate(-25, " + Integer.toString(currentY) + ")"); legend.appendChild(filterType); if (s.length() > longestLabel.length()) longestLabel = s; currentY += labelHeight * 1.3; s = " - " + StringUtils .capitalize(App.prefs.getSampleDepthFilterTypePref(PrefKey.CHART_COMPOSITE_SAMPLE_DEPTH_TYPE, SampleDepthFilterType.MIN_NUM_SAMPLES).toString().toLowerCase() + " >= " + App.prefs.getIntPref(PrefKey.CHART_COMPOSITE_MIN_NUM_SAMPLES, 1)); filterType = legendEB.getDescriptionTextElement(s, leftJustified, (labelHeight / 2)); filterType.setAttributeNS(null, "transform", "translate(-25, " + Integer.toString(currentY) + ")"); legend.appendChild(filterType); labelWidth = FireChartUtil.getStringWidth(Font.PLAIN, 8, longestLabel) + 10; } // Add rectangle around legend and append legend.setAttributeNS(null, "transform", "scale(1.0)"); legend.appendChild(legendEB.getChartRectangle(labelWidth, currentY)); // Only show the chart if the preference has been set to do so if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_LEGEND, true)) { legend.setAttributeNS(null, "display", "inline"); } else { legend.setAttributeNS(null, "display", "none"); } return legend; } /** * Get the time axis including the guide and highlight lines. * * @param height * @return */ private Element getTimeAxis(int height) { // Time axis is centered off of the first year in the reader Element timeAxis = doc.createElementNS(svgNS, "g"); String scale = "scale(" + FireChartUtil.yearsToPixels(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"; timeAxis.setAttributeNS(null, "transform", scale + " translate(-" + this.getFirstChartYear() + ",0)"); int majorTickInterval = App.prefs.getIntPref(PrefKey.CHART_XAXIS_MAJOR_TICK_SPACING, 50); int minorTickInterval = App.prefs.getIntPref(PrefKey.CHART_XAXIS_MINOR_TICK_SPACING, 10); boolean majorTicks = App.prefs.getBooleanPref(PrefKey.CHART_XAXIS_MAJOR_TICKS, true); boolean minorTicks = App.prefs.getBooleanPref(PrefKey.CHART_XAXIS_MINOR_TICKS, true); boolean vertGuides = App.prefs.getBooleanPref(PrefKey.CHART_VERTICAL_GUIDES, true); ArrayList<Integer> years = App.prefs.getIntegerArrayPref(PrefKey.CHART_HIGHLIGHT_YEARS_ARRAY, null); // Add highlight lines if (years != null && App.prefs.getBooleanPref(PrefKey.CHART_HIGHLIGHT_YEARS, false)) { for (Integer i : years) { // Don't plot out of range years if (i > this.getLastChartYear() || i < this.getFirstChartYear()) continue; timeAxis.appendChild(timeAxisEB.getHighlightLine(i, height)); } } for (int i = getFirstChartYear(); i < getLastChartYear(); i++) { if (i % majorTickInterval == 0) { // year is a multiple of tickInterval if (vertGuides) { int vertGuidesOffsetAmount = 0; if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHART_TITLE, true)) { vertGuidesOffsetAmount = App.prefs.getIntPref(PrefKey.CHART_TITLE_FONT_SIZE, 20) + 10; } timeAxis.appendChild(timeAxisEB.getVerticalGuide(i, vertGuidesOffsetAmount, height)); } if (majorTicks) { timeAxis.appendChild(timeAxisEB.getMajorTick(i, height)); } Element year_text_g = doc.createElementNS(svgNS, "g"); year_text_g.setAttributeNS(null, "transform", "translate(" + i + "," + height + ") scale(" + FireChartUtil.pixelsToYears(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"); year_text_g.appendChild(timeAxisEB.getYearTextElement(removeBCYearOffset(i))); timeAxis.appendChild(year_text_g); } if (minorTicks && i % minorTickInterval == 0) // && i % tickInterval != 0) { timeAxis.appendChild(timeAxisEB.getMinorTick(i, height)); } } timeAxis.appendChild(timeAxisEB.getTimeAxis(height)); return timeAxis; } /** * Get the percent scarred plot including bounding box and y2 axis. * * @return */ private Element getPercentScarredPlot() { // determine y scaling double scale_y = -1 * ((double) App.prefs.getIntPref(PrefKey.CHART_INDEX_PLOT_HEIGHT, 100)) / (100); double unscale_y = 1 / scale_y; Element scarred_g = doc.createElementNS(svgNS, "g"); scarred_g.setAttributeNS(null, "id", "scarred"); scarred_g.setAttributeNS(null, "transform", "translate(0," + App.prefs.getIntPref(PrefKey.CHART_INDEX_PLOT_HEIGHT, 100) + ") scale(1," + scale_y + ")"); // only x-scale the drawn part -- not the labels Element scarred_scale_g = doc.createElementNS(svgNS, "g"); scarred_scale_g.setAttributeNS(null, "transform", "scale(" + FireChartUtil.yearsToPixels(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"); scarred_g.appendChild(scarred_scale_g); // draw in vertical bars double[] percent_arr = reader.getPercentOfRecordingScarred( App.prefs.getEventTypePref(PrefKey.CHART_COMPOSITE_EVENT_TYPE, EventTypeToProcess.FIRE_EVENT)); // Limit to specified years if necessary if (!App.prefs.getBooleanPref(PrefKey.CHART_AXIS_X_AUTO_RANGE, true)) { int startindex_file = this.getFirstChartYear() - applyBCYearOffset(reader.getFirstYear()); double[] percent_file = percent_arr.clone(); percent_arr = new double[(this.getLastChartYear() - this.getFirstChartYear()) + 1]; int ind_in_file = startindex_file; for (int ind_in_newarr = 0; ind_in_newarr < percent_arr.length; ind_in_newarr++) { if (ind_in_file > percent_file.length - 1) { // Reached end of data we are extracting break; } if (ind_in_file < 0) { // Before data we are extracting so keep going ind_in_file++; continue; } percent_arr[ind_in_newarr] = percent_file[ind_in_file]; ind_in_file++; } } for (int i = 0; i < percent_arr.length; i++) { if (percent_arr[i] != 0) { double percent = percent_arr[i]; percent = (percent > 100) ? 100 : percent; // don't allow values over 100% scarred_scale_g.appendChild(percentScarredPlotEB.getVerticalLine(i, percent)); } } // draw a rectangle around it // Needs to be 4 lines to cope with stroke width in different coord sys in x and y scarred_scale_g.appendChild(percentScarredPlotEB.getBorderLine1()); scarred_scale_g.appendChild(percentScarredPlotEB.getBorderLine2(unscale_y)); scarred_scale_g.appendChild(percentScarredPlotEB.getBorderLine3()); scarred_scale_g.appendChild(percentScarredPlotEB.getBorderLine4(unscale_y)); // draw in the labels int yAxisFontSize = App.prefs.getIntPref(PrefKey.CHART_AXIS_Y2_FONT_SIZE, 10); int labelHeight = FireChartUtil.getStringHeight(Font.PLAIN, yAxisFontSize, "100"); int labelY = labelHeight / 2; for (int i = 0; i <= 100; i += 25) { Element unscale_g = doc.createElementNS(svgNS, "g"); String x = Double.toString(chartWidth); String y = Integer.toString(i); unscale_g.setAttributeNS(null, "transform", "translate(" + x + "," + y + ") scale(1," + unscale_y + ")"); unscale_g.appendChild(percentScarredPlotEB.getPercentScarredTextElement(labelY, i, yAxisFontSize)); unscale_g.appendChild(percentScarredPlotEB.getHorizontalTick(unscale_y)); scarred_g.appendChild(unscale_g); } // add in the label that says "% Scarred" Element unscale_g = doc.createElementNS(svgNS, "g"); unscale_g.setAttributeNS(null, "transform", "scale(1," + unscale_y + ")"); Element rotate_g = doc.createElementNS(svgNS, "g"); String x = Double .toString(chartWidth + 5 + 10 + FireChartUtil.getStringWidth(Font.PLAIN, yAxisFontSize, "100")); String y = Double.toString(scale_y * 100); rotate_g.setAttributeNS(null, "transform", "translate(" + x + "," + y + ") rotate(90)"); Text label_t = doc.createTextNode(App.prefs.getPref(PrefKey.CHART_AXIS_Y2_LABEL, "% Scarred")); Element label = doc.createElementNS(svgNS, "text"); label.setAttributeNS(null, "font-family", App.prefs.getPref(PrefKey.CHART_FONT_FAMILY, "Verdana")); label.setAttributeNS(null, "font-size", App.prefs.getIntPref(PrefKey.CHART_AXIS_Y2_FONT_SIZE, 10) + ""); label.appendChild(label_t); rotate_g.appendChild(label); unscale_g.appendChild(rotate_g); scarred_g.appendChild(unscale_g); if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_INDEX_PLOT, true) && App.prefs.getBooleanPref(PrefKey.CHART_SHOW_PERCENT_SCARRED, true)) { scarred_g.setAttributeNS(null, "display", "inline"); } else { scarred_g.setAttributeNS(null, "display", "none"); } return scarred_g; } /** * Get the sample or recorder depth plot. * * @param plotSampleNorRecordingDepth * @return */ private Element getSampleOrRecorderDepthsPlot(boolean plotSampleNorRecordingDepth, EventTypeToProcess eventTypeToProcess) { Element sample_g = doc.createElementNS(svgNS, "g"); Element sample_g_chart = doc.createElementNS(svgNS, "g"); // scales the years on the x direction sample_g.setAttributeNS(null, "id", "depths"); int[] sample_depths; if (plotSampleNorRecordingDepth) sample_depths = reader.getSampleDepths(); else sample_depths = reader.getRecordingDepths(eventTypeToProcess); // Limit to specified years if necessary if (!App.prefs.getBooleanPref(PrefKey.CHART_AXIS_X_AUTO_RANGE, true)) { int startindex_file = this.getFirstChartYear() - applyBCYearOffset(reader.getFirstYear()); int[] sample_depths_file = sample_depths.clone(); sample_depths = new int[(this.getLastChartYear() - this.getFirstChartYear()) + 1]; int ind_in_file = startindex_file; for (int ind_in_newarr = 0; ind_in_newarr < sample_depths.length; ind_in_newarr++) { if (ind_in_file > sample_depths_file.length - 1) { // Reached end of data we are extracting break; } if (ind_in_file < 0) { // Before data we are extracting so keep going ind_in_file++; continue; } sample_depths[ind_in_newarr] = sample_depths_file[ind_in_file]; ind_in_file++; } } int[] sample_depths_sorted = sample_depths.clone(); // sort to find the max. Arrays.sort(sample_depths_sorted); int largest_sample_depth = sample_depths_sorted[sample_depths_sorted.length - 1]; // Make the max value 110% of the actually max value so there is a bit of breathing room at top of the chart if (largest_sample_depth > 0) { largest_sample_depth = (int) Math.ceil(largest_sample_depth * 1.1); } else { largest_sample_depth = 1; } // the trend line is constructed as if the first year is 0 A.D. String translation = "translate(0," + App.prefs.getIntPref(PrefKey.CHART_INDEX_PLOT_HEIGHT, 100) + ")"; double scale_y = -1 * ((double) App.prefs.getIntPref(PrefKey.CHART_INDEX_PLOT_HEIGHT, 100)) / (largest_sample_depth); double unscale_y = 1 / scale_y; // String scale = "scale("+yearsToPixels()+"," + scale_y + ")"; String t = translation + "scale(1," + scale_y + ")"; sample_g.setAttributeNS(null, "transform", t); sample_g_chart.setAttributeNS(null, "transform", "scale(" + FireChartUtil.yearsToPixels(chartWidth, getFirstChartYear(), getLastChartYear()) + ",1)"); // error check if (sample_depths.length == 0) { return sample_g; } // build the trend line int begin_index = 0; String lineColor = FireChartUtil .colorToHexString(App.prefs.getColorPref(PrefKey.CHART_SAMPLE_OR_RECORDER_DEPTH_COLOR, Color.BLUE)); for (int i = 1; i < sample_depths.length; i++) { if (sample_depths[i] != sample_depths[begin_index]) { sample_g_chart.appendChild(sampleRecorderPlotEB.getVerticalTrendLinePart(lineColor, i, sample_depths[begin_index], sample_depths[i])); sample_g_chart.appendChild(sampleRecorderPlotEB.getHorizontalTrendLinePart(lineColor, scale_y, begin_index, i, sample_depths[begin_index])); begin_index = i; } // draw in the final line if (i + 1 == sample_depths.length) { sample_g_chart.appendChild(sampleRecorderPlotEB.getHorizontalTrendLinePart(lineColor, scale_y, begin_index, i, sample_depths[begin_index])); } } // add the threshold depth sample_g_chart.appendChild(sampleRecorderPlotEB.getThresholdLine(scale_y, largest_sample_depth)); // add in the tick lines int num_ticks = FireChartUtil.calculateNumSampleDepthTicks(largest_sample_depth); int tick_spacing = (int) Math.ceil((double) largest_sample_depth / (double) num_ticks); int yAxisFontSize = App.prefs.getIntPref(PrefKey.CHART_AXIS_Y1_FONT_SIZE, 10); int labelHeight = FireChartUtil.getStringHeight(Font.PLAIN, yAxisFontSize, "9"); int labelY = labelHeight / 2; for (int i = 0; i < num_ticks; i++) { sample_g.appendChild(sampleRecorderPlotEB.getHorizontalTick(unscale_y, i, tick_spacing)); Element unscale_g = doc.createElementNS(svgNS, "g"); unscale_g.setAttributeNS(null, "transform", "translate(-5," + (i * tick_spacing) + ") scale(1," + (1.0 / scale_y) + ")"); unscale_g.appendChild( sampleRecorderPlotEB.getDepthCountTextElement(labelY, yAxisFontSize, i, tick_spacing)); sample_g.appendChild(unscale_g); } // add in label that says "Sample Depth" int labelWidth = FireChartUtil.getStringWidth(Font.PLAIN, yAxisFontSize, num_ticks * tick_spacing + ""); Element unscale_g = doc.createElementNS(svgNS, "g"); unscale_g.setAttributeNS(null, "transform", "translate(" + (-5 - labelWidth - 10) + "," + 0 + ") scale(1," + (1.0 / scale_y) + ") rotate(270)"); unscale_g.appendChild(sampleRecorderPlotEB.getSampleDepthTextElement()); sample_g.appendChild(unscale_g); sample_g.appendChild(sample_g_chart); if (App.prefs.getBooleanPref(PrefKey.CHART_SHOW_INDEX_PLOT, true)) { sample_g.setAttributeNS(null, "display", "inline"); } else { sample_g.setAttributeNS(null, "display", "none"); } return sample_g; } /** * Replaces the currently displayed (or hidden) chronology plot with a freshly generated chronology plot. */ private void rebuildChronologyPlot() { Element chrono_plot_g = doc.getElementById("chrono_plot_g"); deleteAllChildren(chrono_plot_g); chrono_plot_g.appendChild(getChronologyPlot()); positionSeriesLines(); positionChartGroupersAndDrawTimeAxis(); } /** * Set the visibility of the index plot based on the preferences. */ public void setIndexPlotVisibility() { boolean isVisible = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_INDEX_PLOT, true); Element plot_grouper1 = doc.getElementById("scarred"); Element plot_grouper2 = doc.getElementById("depths"); if (!isVisible) { plot_grouper1.setAttributeNS(null, "display", "none"); plot_grouper2.setAttributeNS(null, "display", "none"); } else { plot_grouper1.setAttributeNS(null, "display", "inline"); plot_grouper2.setAttributeNS(null, "display", "inline"); } positionChartGroupersAndDrawTimeAxis(); } /** * Set the visibility of the chronology plot based on the preferences. */ public void setChronologyPlotVisibility() { boolean isVisible = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHRONOLOGY_PLOT, true); Element plot_grouper = doc.getElementById("chronology_plot"); if (!isVisible) { plot_grouper.setAttributeNS(null, "display", "none"); } else { plot_grouper.setAttributeNS(null, "display", "inline"); } positionChartGroupersAndDrawTimeAxis(); } /** * Set the visibility of the composite plot based on the preferences. */ public void setCompositePlotVisibility() { boolean isVisible = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_COMPOSITE_PLOT, true); Element plot_grouper = doc.getElementById("comp_plot"); if (!isVisible) { plot_grouper.setAttributeNS(null, "display", "none"); } else { plot_grouper.setAttributeNS(null, "display", "inline"); } positionChartGroupersAndDrawTimeAxis(); } /** * Set the visibility of the legend based on the preferences. */ public void setLegendVisibility() { boolean legendVisible = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_LEGEND, true); Element legend = doc.getElementById("legend"); if (legendVisible) { legend.setAttributeNS(null, "display", "inline"); } else { legend.setAttributeNS(null, "display", "none"); } } /** * Set the visibility of the series labels based on the preferences. */ public void setSeriesLabelsVisibility() { boolean isSeriesLabelVisible = App.prefs.getBooleanPref(PrefKey.CHART_SHOW_CHRONOLOGY_PLOT_LABELS, true); for (FHSeriesSVG seriesSVG : seriesSVGList) { Element ser = doc.getElementById("series_label_" + seriesSVG.getTitle()); if (isSeriesLabelVisible) ser.setAttributeNS(null, "display", "inline"); else ser.setAttributeNS(null, "display", "none"); } for (int i = 0; i < seriesSVGList.size(); i++) { Element upButton = doc.getElementById("up_button" + i); Element downButton = doc.getElementById("down_button" + i); if (isSeriesLabelVisible) { upButton.setAttributeNS(null, "display", "inline"); downButton.setAttributeNS(null, "display", "inline"); } else { upButton.setAttributeNS(null, "display", "none"); downButton.setAttributeNS(null, "display", "none"); } } } /** * Set the visibility of the no-export elements based on the input parameter. * * @param isVisible */ public void setVisibilityOfNoExportElements(boolean isVisible) { String visibility_setting = isVisible ? "inline" : "none"; NodeList n = doc.getElementsByTagName("*"); for (int i = 0; i < n.getLength(); i++) { Element temp = (Element) n.item(i); if (temp.getAttribute("class").equals("no_export")) { temp.setAttributeNS(null, "display", visibility_setting); } } } /** * This function toggles the visibility of the series at the given location. * * @param index of the series to hide */ public void toggleVisibilityOfSeries(int index) { FHSeriesSVG seriesToHide = seriesSVGList.get(index); seriesToHide.toggleVisibility(); seriesSVGList.set(index, seriesToHide); positionSeriesLines(); positionChartGroupersAndDrawTimeAxis(); } // ============== Annotation ============== // There is a <rect id="annote_canvas"> element in the DOM under <g id="annote_g">. // It is used to catch mouse events in order to add, resize, or delete annotation rectangles. // The enum Mode is used to track whether the user has selected add, resize, &etc. // All mode checking will be done java-side to simplify the js. // In other words, rectangles will always call deleteAnnoteRect when clicked, and it is up // to deleteAnnoteRect to ensure that the rect only gets deleted when the user is in the eraser mode // ======================================== /** * Gets the annote canvas as an element. * * @return annote_canvas */ public Element getAnnoteCanvas() { try { Element annote_canvas = doc.createElementNS(svgNS, "rect"); annote_canvas.setAttributeNS(null, "id", "annote_canvas"); annote_canvas.setAttributeNS(null, "width", this.chartWidth + ""); annote_canvas.setAttributeNS(null, "height", "999"); annote_canvas.setAttributeNS(null, "onmousedown", "paddingGrouperOnClick(evt)"); annote_canvas.setAttributeNS(null, "opacity", "0.0"); return annote_canvas; } catch (BridgeException e) { e.printStackTrace(); } return null; } /** * Draws a line on the annotation grouper. * * @param x * @return */ public String drawAnnoteLine(int x) { if (annotemode == AnnoteMode.LINE) { Element annote_g = doc.getElementById("annote_g"); Element annote_canvas = doc.getElementById("annote_canvas"); Element annote_line = doc.createElementNS(svgNS, "line"); SVGRect annote_canvas_rc = SVGLocatableSupport.getBBox(annote_canvas); String id = "annote_line_" + (lineGensym++); annote_line.setAttributeNS(null, "id", id); annote_line.setAttributeNS(null, "x1", Float.toString(x - chartXOffset)); annote_line.setAttributeNS(null, "y1", "0"); annote_line.setAttributeNS(null, "x2", Float.toString(x - chartXOffset)); annote_line.setAttributeNS(null, "y2", Float.toString(annote_canvas_rc.getHeight())); annote_line.setAttributeNS(null, "stroke", "black"); annote_line.setAttributeNS(null, "stroke-width", "3"); annote_line.setAttributeNS(null, "opacity", "0.5"); annote_line.setAttributeNS(null, "onmousedown", "FireChartSVG.getChart(chart_num).deleteAnnoteLine('" + id + "')"); annote_g.appendChild(annote_line); annotemode = AnnoteMode.NONE; return id; } return "wrong_annotemode"; } /** * Removes a line from the annotation grouper. * * @param id * @return */ public boolean deleteAnnoteLine(String id) { if (annotemode == AnnoteMode.ERASE) { Element annote_g = doc.getElementById("annote_g"); Element annote_line = doc.getElementById(id); if (annote_line == null) { return false; } annote_g.removeChild(annote_line); return true; } return false; } /** * Sets the annote mode according to the input parameter. * * @param m */ public void setAnnoteMode(AnnoteMode m) { annotemode = m; // Changing the cursor would be cool, but I couldn't get it // to load a custom graphic. A base-64 encoded directly in // this files didn't work either. // Element annote_canvas = doc.getElementById("annote_canvas"); // if( annotemode == AnnoteMode.LINE ) { // annote_canvas.setAttributeNS(null, "cursor", "move"); // } // else { // annote_canvas.setAttributeNS(null, "cursor", "auto"); // } } /** * Returns a dimension in years (time coordinate system) for the specified proportion of the chart width. * * @param prop * @return */ public Double standardChartUnits(int prop) { Double pixelsForProportion = this.chartWidth * (prop / 1000.0); return FireChartUtil.pixelsToYears(pixelsForProportion, chartWidth, getFirstChartYear(), getLastChartYear()); } /** * Handles printing of the SVG document. * * @param doc * @param out */ public static void printDocument(Document doc, OutputStream out) { try { TransformerFactory tf = TransformerFactory.newInstance(); Transformer t = tf.newTransformer(); t.setOutputProperty(OutputKeys.METHOD, "xml"); t.setOutputProperty(OutputKeys.INDENT, "yes"); t.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(out, "UTF-8"))); log.debug("Document printed successfully."); } catch (Exception ex) { System.out.println("Error: Could not printDocument\n"); } } /** * Sorts the series according to the sort by preference. */ private void sortSeriesAccordingToPreference() { String sortByPreference = App.prefs.getPref(PrefKey.CHART_SORT_BY_PREFERENCE, SeriesSortType.NAME.toString()); if (sortByPreference.equals(SeriesSortType.NAME.toString())) { sortByName(); } else if (sortByPreference.equals(SeriesSortType.CATEGORY.toString())) { sortByCategory(); } else if (sortByPreference.equals(SeriesSortType.FIRST_FIRE_YEAR.toString())) { sortByFirstFireYear(); } else if (sortByPreference.equals(SeriesSortType.SAMPLE_START_YEAR.toString())) { sortBySampleStartYear(); } else if (sortByPreference.equals(SeriesSortType.SAMPLE_END_YEAR.toString())) { sortBySampleEndYear(); } else { sortByPositionInFile(); } } /** * Sort the series by name. */ public void sortByName() { Comparator<FHSeriesSVG> comparator = new Comparator<FHSeriesSVG>() { @Override public int compare(FHSeriesSVG c1, FHSeriesSVG c2) { return c1.getTitle().compareTo(c2.getTitle()); } }; Collections.sort(seriesSVGList, comparator); lastTypeSortedBy = SeriesSortType.NAME; rebuildChronologyPlot(); log.debug("Finished sorting chart series by name"); } /** * Sort the series by category. Currently this only sorts by the first entry of a series. This will need to be changed once the TRIDAS * format is implemented. TODO */ public void sortByCategory() { Comparator<FHSeriesSVG> comparator = new Comparator<FHSeriesSVG>() { @Override public int compare(FHSeriesSVG c1, FHSeriesSVG c2) { String c1_first_category_entry = c1.getCategoryEntries().get(0).getContent(); String c2_first_category_entry = c2.getCategoryEntries().get(0).getContent(); return c1_first_category_entry.compareTo(c2_first_category_entry); } }; Collections.sort(seriesSVGList, comparator); lastTypeSortedBy = SeriesSortType.CATEGORY; rebuildChronologyPlot(); log.debug("Finished sorting chart series by category"); } /** * Sort the series by first fire year. */ public void sortByFirstFireYear() { Comparator<FHSeriesSVG> comparator = new Comparator<FHSeriesSVG>() { @Override public int compare(FHSeriesSVG c1, FHSeriesSVG c2) { boolean[] c1_events = c1.getEventYears(); boolean[] c2_events = c2.getEventYears(); int i = 0; for (i = 0; i < c1_events.length && !c1_events[i]; i++) { ; // loop until the index of the first fire year for c1 is found } int c1_first_fire_year = applyBCYearOffset(c1.getFirstYear()) + i; int j = 0; for (j = 0; j < c2_events.length && !c2_events[j]; j++) { ; // loop until the index of the first fire year for c2 is found } int c2_first_fire_year = applyBCYearOffset(c2.getFirstYear()) + j; return c2_first_fire_year - c1_first_fire_year; } }; Collections.sort(seriesSVGList, comparator); lastTypeSortedBy = SeriesSortType.FIRST_FIRE_YEAR; rebuildChronologyPlot(); log.debug("Finished sorting chart series by first fire year"); } /** * Sort the series by start year. */ public void sortBySampleStartYear() { Comparator<FHSeriesSVG> comparator = new Comparator<FHSeriesSVG>() { @Override public int compare(FHSeriesSVG c1, FHSeriesSVG c2) { return applyBCYearOffset(c2.getFirstYear()) - applyBCYearOffset(c1.getFirstYear()); } }; Collections.sort(seriesSVGList, comparator); lastTypeSortedBy = SeriesSortType.SAMPLE_START_YEAR; rebuildChronologyPlot(); log.debug("Finished sorting chart series by series start year"); } /** * Sort the series by end year. */ public void sortBySampleEndYear() { Comparator<FHSeriesSVG> comparator = new Comparator<FHSeriesSVG>() { @Override public int compare(FHSeriesSVG c1, FHSeriesSVG c2) { return applyBCYearOffset(c2.getLastYear()) - applyBCYearOffset(c1.getLastYear()); } }; Collections.sort(seriesSVGList, comparator); lastTypeSortedBy = SeriesSortType.SAMPLE_END_YEAR; rebuildChronologyPlot(); log.debug("Finished sorting chart series by series end year"); } /** * Sort the series by end year. */ public void sortByPositionInFile() { Comparator<FHSeriesSVG> comparator = new Comparator<FHSeriesSVG>() { @Override public int compare(FHSeriesSVG c1, FHSeriesSVG c2) { Integer c1pos = c1.getSequenceInFile(); Integer c2pos = c2.getSequenceInFile(); return c1pos.compareTo(c2pos); } }; Collections.sort(seriesSVGList, comparator); lastTypeSortedBy = SeriesSortType.AS_IN_FILE; rebuildChronologyPlot(); log.debug("Finished sorting chart series by position in file"); } // public boolean setCommonTickAttrib(int weight, Color color, LineStyle style) { // // tickLineWeight = weight; // tickLineStyle = style; // setTickColor(color); // positionChartGroupersAndDrawTimeAxis(); // return false; // } }