package gdsc.smlm.ij.plugins;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2013 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* 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.
*---------------------------------------------------------------------------*/
import gdsc.smlm.ij.settings.FilterSettings;
import gdsc.smlm.ij.settings.GlobalSettings;
import gdsc.smlm.ij.settings.SettingsManager;
import gdsc.core.ij.Utils;
import gdsc.core.utils.TextUtils;
import gdsc.smlm.utils.XmlUtils;
import gdsc.smlm.results.filter.AndFilter;
import gdsc.smlm.results.filter.Filter;
import gdsc.smlm.results.filter.OrFilter;
import gdsc.smlm.results.filter.PrecisionFilter;
import gdsc.smlm.results.filter.SNRFilter;
import gdsc.smlm.results.filter.WidthFilter;
import ij.IJ;
import ij.gui.GenericDialog;
import ij.plugin.PlugIn;
import java.awt.Checkbox;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Creates an XML file of configured filters from a template.
*/
public class CreateFilters implements PlugIn, ItemListener
{
private static final String TITLE = "Create Filters";
private static boolean enumerateEarly = true;
private GlobalSettings gs;
private FilterSettings filterSettings;
private Pattern pattern = Pattern.compile("(\\S+)=\"(\\S+):(\\S+):(\\S+)\"(\\S*)");
/*
* (non-Javadoc)
*
* @see ij.plugin.PlugIn#run(java.lang.String)
*/
public void run(String arg)
{
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
if (!showDialog())
return;
// This method assumes valid XML elements have been input with no root node.
// The output will be an expanded set of the same XML elements with specific
// attributes updated.
// Each top level element is processed. All the attributes are scanned and if
// they contain a 'min:max:increment' token the attribute is enumerated to
// the output XML, replicating the element.
// Add a dummy root element to allow the XML to be loaded as a document
String xml = "<root>" + filterSettings.filterTemplate + "</root>";
// Load the XML as a document
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
IJ.showStatus("Creating filters");
try
{
DocumentBuilder db = dbf.newDocumentBuilder();
Document dom = db.parse(new InputSource(new StringReader(xml)));
Element docElement = dom.getDocumentElement();
StringWriter sw = new StringWriter();
sw.write("<linked-list>");
// For each element
int childCount = docElement.getChildNodes().getLength();
int total = 0;
for (int c = 0; c < childCount; c++)
{
Node node = docElement.getChildNodes().item(c);
if (node.getNodeType() != Node.ELEMENT_NODE)
continue;
total += processElement(sw, node);
}
sw.write("</linked-list>");
if (total > 0)
saveFilters(sw, total);
else
IJ.error(TITLE, "No filters created");
}
catch (Exception e)
{
IJ.error(TITLE, "Unable to load the input XML:\n" + e.getMessage());
IJ.showStatus("");
}
}
private int processElement(StringWriter sw, Node node)
throws TransformerFactoryConfigurationError, TransformerException
{
// Get entire element as a string
String xmlString = XmlUtils.getString(node, false);
ArrayList<StringBuilder> out = new ArrayList<StringBuilder>();
// Process through the XML appending to the current output.
String[] tokens = xmlString.split("\\s+");
for (String token : tokens)
{
// Any attributes with enumerations should be expanded.
Matcher match = pattern.matcher(token);
if (match.find())
{
final String prefix = " " + match.group(1) + "=\"";
// Use Big Decimal for the enumeration to preserve the precision of the input text
// (i.e. using doubles for an enumeration can lose precision and fail to correctly enumerate)
BigDecimal min, max, inc;
try
{
min = new BigDecimal(match.group(2));
max = new BigDecimal(match.group(3));
inc = new BigDecimal(match.group(4));
if (min.compareTo(max) > 0 || inc.compareTo(BigDecimal.ZERO) <= 0)
throw new RuntimeException("Invalid 'min:max:increment' attribute: " + token);
}
catch (NumberFormatException e)
{
throw new RuntimeException("Invalid 'min:max:increment' attribute: " + token, e);
}
final String suffix = "\"" + match.group(5);
// Enumerate the attribute
ArrayList<String> attributeText = new ArrayList<String>();
for (BigDecimal bd = min; bd.compareTo(max) <= 0; bd = bd.add(inc))
attributeText.add(prefix + bd.toString() + suffix);
// Add to the current output
ArrayList<StringBuilder> out2 = new ArrayList<StringBuilder>(out.size() * attributeText.size());
if (enumerateEarly)
{
// Enumerate earlier attributes first
for (String text : attributeText)
{
for (StringBuilder sb : out)
{
out2.add(new StringBuilder(sb.toString()).append(text));
}
}
}
else
{
// Enumerate later attributes first
for (StringBuilder sb : out)
{
final String current = sb.toString();
for (String text : attributeText)
{
out2.add(new StringBuilder(current).append(text));
}
}
}
out = out2;
}
else
{
if (out.isEmpty())
out.add(new StringBuilder(token));
else
{
for (StringBuilder sb : out)
sb.append(" ").append(token);
}
}
}
if (out.size() > 0)
{
sw.write("<FilterSet name=\"");
sw.write(getName(out.get(0)));
sw.write("\"><filters class=\"linked-list\">");
for (StringBuilder sb : out)
sw.write(sb.toString());
sw.write("</filters></FilterSet>");
}
return out.size();
}
private String getName(StringBuilder sb)
{
Filter f = Filter.fromXML(sb.toString());
if (f != null)
return f.getType().replaceAll("&", "&");
return "";
}
private void saveFilters(StringWriter sw, int total)
{
// Save the output to file
IJ.showStatus("Saving filters");
String filename = Utils.getFilename("Filter_File", filterSettings.filterSetFilename);
if (filename != null)
{
OutputStreamWriter out = null;
try
{
filterSettings.filterSetFilename = filename;
// Append .xml if no suffix
if (filename.lastIndexOf('.') < 0)
filterSettings.filterSetFilename += ".xml";
FileOutputStream fos = new FileOutputStream(filterSettings.filterSetFilename);
out = new OutputStreamWriter(fos, "UTF-8");
out.write(XmlUtils.prettyPrintXml(sw.toString()));
SettingsManager.saveSettings(gs);
IJ.showStatus(total + " filters: " + filterSettings.filterSetFilename);
}
catch (Exception e)
{
IJ.log("Unable to save the filter sets to file: " + e.getMessage());
}
finally
{
if (out != null)
{
try
{
out.close();
}
catch (IOException e)
{
// Ignore
}
}
}
}
}
private boolean showDialog()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addHelp(About.HELP_URL);
gd.addMessage(
"Create a set of filters for use in the Filter Analysis plugin.\nAttributes will be enumerated if they are of the form 'min:max:increment'");
gs = SettingsManager.loadSettings();
filterSettings = gs.getFilterSettings();
gd.addTextAreas(filterSettings.filterTemplate, null, 20, 80);
gd.addCheckbox("Enumerate_early attributes first", enumerateEarly);
gd.addCheckbox("Show_demo_filters", false);
if (Utils.isShowGenericDialog())
{
Checkbox cb = (Checkbox) gd.getCheckboxes().get(1);
cb.addItemListener(this);
}
gd.showDialog();
if (gd.wasCanceled())
return false;
filterSettings.filterTemplate = gd.getNextText();
enumerateEarly = gd.getNextBoolean();
boolean demoFilters = gd.getNextBoolean();
if (demoFilters)
{
logDemoFilters();
return false;
}
return SettingsManager.saveSettings(gs);
}
public void itemStateChanged(ItemEvent e)
{
// When the checkbox is clicked, output the list of available filters to the ImageJ log
Checkbox cb = (Checkbox) e.getSource();
if (cb.getState())
{
cb.setState(false);
logDemoFilters();
}
}
private void logDemoFilters()
{
comment(TITLE + " example template filters");
IJ.log("");
comment("Filters are described using XML");
comment("Filter attibutes that take the form 'min:max:increment' will be enumerated");
comment("Note: This example is a subset. All filters are described in the user manual");
IJ.log("");
comment("Single filters");
IJ.log("");
demo(new SNRFilter(10), new String[] { "10:20:1" });
demo(new PrecisionFilter(30), new String[] { "30:50:2" });
IJ.log("");
comment("Combined filters");
IJ.log("");
demo(new AndFilter(new SNRFilter(10), new WidthFilter(2)), new String[] { "10:20:1", "1.5:2.5:0.2" });
demo(new OrFilter(new PrecisionFilter(30), new AndFilter(new SNRFilter(10), new WidthFilter(2))),
new String[] { "30:40:2", "10:20:1", "1.5:2.5:0.2" });
IJ.log("");
}
private void demo(Filter filter, String... attributeSubstitutions)
{
// Create the filter XML
String xml = filter.toXML();
if (attributeSubstitutions != null)
{
// Process the XML substituting attributes in the order they occur using a SAX parser.
// Write the new XMl to a buffer.
StringBuilder sb = new StringBuilder();
try
{
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)),
new AttributeSubstitutionHandler(sb, attributeSubstitutions));
}
catch (Exception e)
{
e.printStackTrace();
}
//IJ.log(xml);
//IJ.log(sb.toString());
xml = sb.toString();
}
IJ.log(XmlUtils.prettyPrintXml(xml));
}
/*
* Inner class for the Callback Handlers. This class replaces the attributes in the XML with the given
* substitutions. Attributes are processed in order. Substitutions are ignored (skipped) if they are null.
*/
class AttributeSubstitutionHandler extends DefaultHandler
{
StringBuilder sb;
String[] attributeSubstitutions;
int substitutionCount = 0;
public AttributeSubstitutionHandler(StringBuilder sb, String[] attributeSubstitutions)
{
this.sb = sb;
this.attributeSubstitutions = attributeSubstitutions;
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String,
* org.xml.sax.Attributes)
*
* Only start elements have attributes so this is where the substitutions are made
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
{
sb.append("<").append(qName);
for (int attribute = 0; attribute < attributes.getLength(); attribute++)
{
sb.append(" ");
String name = attributes.getQName(attribute);
if (substitutionCount < attributeSubstitutions.length && !name.equals("class"))
{
String nextSubstitution = attributeSubstitutions[substitutionCount++];
if (nextSubstitution != null)
{
sb.append(name).append("=\"").append(nextSubstitution).append("\"");
continue;
}
}
sb.append(name).append("=\"").append(attributes.getValue(attribute)).append("\"");
}
sb.append(">");
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*
* We must respect the end elements since combined filters require them. They can be later stripped using a
* pretty print XML method.
*/
@Override
public void endElement(String uri, String localName, String qName) throws SAXException
{
sb.append("</").append(qName).append(">");
}
}
private void comment(String text)
{
IJ.log(TextUtils.wrap("<!-- " + text + " -->", 80));
}
}