package ini.trakem2.io;
import ij.measure.Calibration;
import ini.trakem2.display.AreaTree;
import ini.trakem2.display.Connector;
import ini.trakem2.display.Displayable;
import ini.trakem2.display.Node;
import ini.trakem2.display.Tree;
import ini.trakem2.display.Treeline;
import ini.trakem2.utils.Utils;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
public final class NeuroML {
/** Given a branch or end node, gather the list of nodes all the way
* up to the previous branch node or root (not included).
* The list has be as the first node. */
static private final <T> List<Node<T>> cable(final Node<T> be) {
final ArrayList<Node<T>> slab = new ArrayList<Node<T>>();
slab.add(be);
// Collect nodes up to a node (not included)
// that must have a parent (so not the root)
// and more than one child.
Node<T> p1 = be.getParent();
Node<T> p2 = null == p1 ? null : p1.getParent();
while (p2 != null && 1 == p1.getChildrenCount()) {
slab.add(p1);
p1 = p2;
p2 = p2.getParent();
}
return slab;
}
static private final void writeCellHeader(final Writer w, final Tree<?> t) throws IOException {
w.write("<cell name=\""); w.write(Long.toString(t.getId())); w.write("\">\n");
w.write(" <meta:notes>");
w.write(t.getProject().getMeaningfulTitle(t));
final String annotation = t.getAnnotation();
if (null != annotation) {
w.write("\n");
w.write(t.getAnnotation());
}
w.write("</meta:notes>\n");
w.write(" <meta:properties>\n");
w.write(" <meta:property tag=\"Neuron type\" value=\"manually reconstructed\" />\n");
w.write(" </meta:properties>\n");
w.write(" <segments xmlns=\"http://morphml.org/morphml/schema\">\n");
}
/** Transform in 2d the point with the given affine that combines the Tree affine and the calibration,
* along with its z and radius.
*
* Then the data is stored into fp as:
* fp[0] = x
* fp[1] = y
* fp[2] = z
* fp[3] = radius
*/
static private final void toPoint(final Node<Float> nd, final float[] fp, final AffineTransform aff, final double zScale) {
// 0,1: the point
fp[0] = nd.getX();
fp[1] = nd.getY();
// 2,3: the point with x displaced by the radius
fp[2] = fp[0] + nd.getData();
fp[3] = fp[1];
//
aff.transform(fp, 0, fp, 0, 2);
// Compute transformed radius
fp[3] = (float) Math.sqrt(Math.pow(fp[2] - fp[0], 2) + Math.pow(fp[3] - fp[1], 2));
// Set Z
fp[2] = (float)(nd.getLayer().getZ() * zScale);
}
static private final void writeSomaSegment(final Writer w, final float[] root) throws IOException {
w.write(" <segment id=\"0\" name=\"0\" cable=\"0\">\n");
final String sx = Float.toString(root[0]),
sy = Float.toString(root[1]),
sz = Float.toString(root[2]),
sd = Float.toString(Math.max(1, 2 * root[3])); // it's a radius, and at least the diameter should be 1
w.write(" <proximal x=\""); w.write(sx);
w.write("\" y=\""); w.write(sy);
w.write("\" z=\""); w.write(sz);
w.write("\" diameter=\""); w.write(sd);
w.write("\"/>\n <distal x=\""); w.write(sx);
w.write("\" y=\""); w.write(sy);
w.write("\" z=\""); w.write(sz);
w.write("\" diameter=\""); w.write(sd);
w.write("\"/>\n </segment>\n");
}
static private final void writeCableSegment(final Writer w,
final float[] seg, final long segId,
final long parentId, final float[] parentCoords,
final String sCableId) throws IOException
{
final String sid = Long.toString(segId);
w.write(" <segment id=\""); w.write(sid); w.write("\" name=\""); w.write(sid);
w.write("\" parent=\""); w.write(Long.toString(parentId));
w.write("\" cable=\""); w.write(sCableId);
w.write("\">\n");
if (null != parentCoords) {
w.write(" <proximal x=\""); w.write(Float.toString(parentCoords[0]));
w.write("\" y=\""); w.write(Float.toString(parentCoords[1]));
w.write("\" z=\""); w.write(Float.toString(parentCoords[2]));
w.write("\" diameter=\""); w.write(Float.toString(Math.max(1, parentCoords[3])));
w.write("\"/>\n");
}
w.write(" <distal x=\""); w.write(Float.toString(seg[0]));
w.write("\" y=\""); w.write(Float.toString(seg[1]));
w.write("\" z=\""); w.write(Float.toString(seg[2]));
w.write("\" diameter=\""); w.write(Float.toString(Math.max(1, seg[3]))); // at least 1
w.write("\"/>\n </segment>\n");
}
static private final class HalfSynapse {
private Connector c;
private Tree<?> t;
@SuppressWarnings("unused")
private Node<?> node;
private long segmentId;
/** A pre- or a post-synaptic site, located at {@code node} with which a segment with id {@code segmentId} was made.*/
HalfSynapse(final Connector c, final Tree<?> t, final Node<?> node, final long segmentId) {
this.c = c;
this.t = t;
this.node = node;
this.segmentId = segmentId;
}
}
static private final class Synapse {
private final HalfSynapse pre, post;
Synapse(final HalfSynapse pre, final HalfSynapse post) {
this.pre = pre;
this.post = post;
}
}
static private final void collectConnectors(final Node<?> node, final Tree<?> t,
final float[] nodeWorldCoords, final long segmentId,
final List<HalfSynapse> pre, final List<HalfSynapse> post)
{
for (final Displayable d : t.getLayerSet().findZDisplayables(Connector.class, node.getLayer(), (int)nodeWorldCoords[0], (int)nodeWorldCoords[1], false)) {
final Connector c = (Connector) d;
if (c.intersectsOrigin(nodeWorldCoords[0], nodeWorldCoords[1], node.getLayer())) {
pre.add(new HalfSynapse(c, t, node, segmentId));
} else {
post.add(new HalfSynapse(c, t, node, segmentId));
}
}
}
static private final class TreePair {
final Tree<?> source, target;
TreePair(Tree<?> source, Tree<?> target) {
this.source = source;
this.target = target;
}
public final boolean equals(final Object ob) {
final TreePair p = (TreePair) ob;
return (source == p.source && target == p.target);
}
}
static private final double scaleToMicrometers(final Calibration cal) {
final double scale;
final String unit = cal.getUnit().trim().toLowerCase();
if (unit.equals("nanometer") || unit.equals("nm")) scale = 0.001;
else if (unit.equals("micrometer") || unit.equals("µm") || unit.equals("um")) scale = 1;
else if (unit.equals("milimeter") || unit.equals("mm")) scale = 1000;
else if (unit.equals("meter") || unit.equals("m")) scale = 1000000;
else {
scale = 1;
Utils.logAll("UNKNOWN unit '" + unit + "' -- using scale of 1.");
}
return scale;
}
static public final void exportMorphML(final Collection<Tree<?>> trees, final Writer w) throws Exception {
exportMorphML(new HashSet<Tree<?>>(trees), w);
}
static public final void exportMorphML(final Set<Tree<?>> trees, final Writer w) throws Exception {
if (trees.isEmpty()) return;
// Header
w.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
w.write("<!-- Exported from TrakEM2 '" + Utils.version + "' at " + new Date() + "\nTrakEM2 software by Albert Cardona, Institute of Neuroinformatics of the University of Zurich and ETH Zurich -->\n");
w.write("<morphml xmlns=\"http://morphml.org/morphml/schema\"\n");
w.write(" xmlns:meta=\"http://morphml.org/metadata/schema\"\n");
w.write(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n");
w.write(" xsi:schemaLocation=\"http://morphml.org/morphml/schema http://www.neuroml.org/NeuroMLValidator/NeuroMLFiles/Schemata/v1.8.1/Level1/MorphML_v1.8.1.xsd\"\n");
w.write(" length_units=\"micrometer\">\n<cells>\n");
// Scale units to micrometers
final Calibration cal = trees.iterator().next().getLayerSet().getCalibration();
final double scale = scaleToMicrometers(cal);
final AffineTransform scale2d = new AffineTransform(cal.pixelWidth * scale, 0, 0, cal.pixelHeight * scale, 0, 0);
final double zScale = cal.pixelWidth * scale; // not pixelDepth
// Each Tree is a cell
for (final Tree<?> t : trees)
{
if (null == t.getRoot()) continue;
exportMorphMLCell(w, t, trees, null, null, scale2d, zScale);
}
w.write("</cells>\n</morphml>\n");
}
static public final void exportNeuroML(final Collection<Tree<?>> trees, final Writer w) throws Exception {
exportNeuroML(new HashSet<Tree<?>>(trees), w);
}
/** Export to NeuroML 1.8.3, with synapses.
* Every {@link Tree} is represented by a <cell>, and an instance of that <cell>
* is represented by a <population> of one single cell. */
static public final void exportNeuroML(final Set<Tree<?>> trees, final Writer w) throws Exception
{
if (trees.isEmpty()) return;
// Header
w.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<!-- Exported from TrakEM2 '" + Utils.version + "' at " + new Date() + "\nTrakEM2 software by Albert Cardona, Institute of Neuroinformatics of the University of Zurich and ETH Zurich -->\n"
+ "<neuroml xmlns=\"http://morphml.org/neuroml/schema\"\n"
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " xmlns:net=\"http://morphml.org/networkml/schema\"\n"
+ " xmlns:mml=\"http://morphml.org/morphml/schema\"\n"
+ " xmlns:meta=\"http://morphml.org/metadata/schema\"\n"
+ " xmlns:bio=\"http://morphml.org/biophysics/schema\"\n"
+ " xmlns:cml=\"http://morphml.org/channelml/schema\"\n"
+ " xsi:schemaLocation=\"http://morphml.org/neuroml/schema http://www.neuroml.org/NeuroMLValidator/NeuroMLFiles/Schemata/v1.8.1/Level3/NeuroML_Level3_v1.8.1.xsd\"\n"
+ " length_units=\"micrometer\">\n");
final List<HalfSynapse> presynaptic = new ArrayList<HalfSynapse>();
final List<HalfSynapse> postsynaptic = new ArrayList<HalfSynapse>();
// Scale units to micrometers
final Calibration cal = trees.iterator().next().getLayerSet().getCalibration();
final double scale = scaleToMicrometers(cal);
final AffineTransform scale2d = new AffineTransform(cal.pixelWidth * scale, 0, 0, cal.pixelHeight * scale, 0, 0);
final double zScale = cal.pixelWidth * scale; // not pixelDepth
w.write("<cells>\n");
// Each Tree is a cell
for (final Tree<?> t : trees)
{
if (null == t.getRoot()) continue;
exportMorphMLCell(w, t, trees, presynaptic, postsynaptic, scale2d, zScale);
}
w.write("</cells>\n");
// Write a a population of cell for every Tree, where each population has only one cell at 0,0,0.
// If the id=10, then the name is p10 and the type is t10.
w.write("<populations xmlns=\"http://morphml.org/networkml/schema\">\n");
for (final Tree<?> t : trees) {
w.write(" <population name=\"p");
final String sid = Long.toString(t.getId());
w.write(sid);
w.write("\" cell_type=\"t");
w.write(sid);
w.write("\">\n <instances size=\"1\">\n <instance id=\"0\"><location x=\"0\" y=\"0\" z=\"0\"/></instance>\n </instances>\n </population>\n");
}
w.write("</populations>\n");
// Write a project group with all the synapses among the members of the set of trees.
w.write("<projections units=\"Physiological Units\" xmlns=\"http://morphml.org/networkml/schema\">\n");
// Figure out which pre connect to which post: the Connector instance is shared, so use it as key
final Map<Connector,HalfSynapse> cpre = new HashMap<Connector,HalfSynapse>();
for (final HalfSynapse syn : presynaptic) {
cpre.put(syn.c, syn);
}
final Map<TreePair,List<Synapse>> pairs = new HashMap<TreePair,List<Synapse>>();
for (final HalfSynapse post : postsynaptic) {
final HalfSynapse pre = cpre.get(post.c);
if (null == pre) continue; // Does not originate within the set of trees
// pre and post share the same Connector
final TreePair pair = new TreePair(pre.t, post.t);
List<Synapse> ls = pairs.get(pair);
if (null == ls) {
ls = new ArrayList<Synapse>();
pairs.put(pair, ls);
}
ls.add(new Synapse(pre, post));
}
for (final Map.Entry<TreePair,List<Synapse>> e : pairs.entrySet()) {
// Write synapse between pre and post
final TreePair pair = e.getKey();
w.write(" <projection name=\"NetworkConnection\" source=\"p");
w.write(Long.toString(pair.source.getId()));
w.write("\" target=\"p");
w.write(Long.toString(pair.target.getId()));
w.write("\">\n");
w.write(" <synapse_props synapse_type=\"DoubExpSynA\" internal_delay=\"5\" weight=\"1\" threshold=\"-20\"/>\n");
w.write(" <connections size=\"");
final List<Synapse> ls = e.getValue();
w.write(Integer.toString(ls.size()));
w.write("\">\n");
int cid = 0;
for (final Synapse syn : ls) {
w.write(" <connection id=\"");
w.write(Integer.toString(cid));
w.write("\" pre_cell_id=\"0\" pre_segment_id=\"");
w.write(Long.toString(syn.pre.segmentId));
w.write("\" pre_fraction_along=\"0.5\" post_cell_id=\"0\" post_segment_id=\"");
w.write(Long.toString(syn.post.segmentId));
w.write("\"/>\n");
cid += 1;
}
w.write(" </connections>\n");
w.write(" </projection>\n");
}
w.write(" </projections>\n");
w.write("</neuroml>\n");
}
/** Without headers, just the cell block for a single AreaTree.
* Works by duplicating the AreaTree as a Treeline. */
static private final void exportMorphMLCell(final Writer w, final Tree<?> t,
final Set<Tree<?>> trees, final List<HalfSynapse> pre, final List<HalfSynapse> post,
final AffineTransform scale2d, final double zScale) throws Exception
{
if (t instanceof Treeline) {
exportMorphMLCell(w, (Treeline)t, trees, pre, post, scale2d, zScale);
} else if (t instanceof AreaTree) {
exportMorphMLCell(w, Tree.copyAs((AreaTree)t, Treeline.class, Treeline.RadiusNode.class), trees, pre, post, scale2d, zScale);
}
}
/** Without headers, just the cell block for a single Treeline.
* If pre is null, then synapses are not collected. */
static private final void exportMorphMLCell(final Writer w, final Treeline t,
final Set<Tree<?>> trees, final List<HalfSynapse> pre, final List<HalfSynapse> post,
final AffineTransform scale2d, final double zScale) throws IOException
{
final float[] fp = new float[4]; // x, y, z, r
// Prepare transform
final AffineTransform aff = new AffineTransform(t.getAffineTransform());
aff.preConcatenate(scale2d);
writeCellHeader(w, t);
// Map of Node vs id of the node
// These ids are used to express parent-child relationships between segments
final HashMap<Node<Float>,Long> nodeIds = new HashMap<Node<Float>,Long>();
// Map of coords for branch or end nodes
// so that the start of a cable can write the proximal coords
final HashMap<Node<Float>,float[]> nodeCoords = new HashMap<Node<Float>,float[]>();
// Root gets ID of 0:
long nextSegmentId = 0;
long cableId = 0;
final Node<Float> root = t.getRoot();
toPoint(root, fp, aff, zScale);
writeSomaSegment(w, fp); // a dummy segment that has no length, and with a cableId of 0.
if (null != pre) collectConnectors(root, t, fp, 0, pre, post);
// Prepare
nodeIds.put(root, nextSegmentId);
nodeCoords.put(root, fp.clone());
nextSegmentId += 1;
cableId += 1;
// All cables that come out of the Soma (the root) require a special tag:
final HashSet<Long> somaCables = new HashSet<Long>();
// Iterate all cables (all slabs; here a slab is synonym with cable, even if in NeuroML it doesn't have to be)
for (final Node<Float> node : t.getRoot().getBranchAndEndNodes()) {
// Gather the list of nodes all the way up to the previous branch node or root,
// that last one not included.
final List<Node<Float>> slab = cable(node);
final String sCableId = Long.toString(cableId);
// The id of the parent already exists, given that the Collection
// is iterated depth-first from the root.
final Node<Float> parent = slab.get(slab.size()-1).getParent();
long parentId = nodeIds.get(parent);
// Use the parent coords for the proximal coords of the first segment of the cable
float[] parentCoords = nodeCoords.get(parent);
// Is it a cable coming out of the root node (the soma) ?
if (0 == parentId) somaCables.add(cableId);
// For every node starting from the closest to the root (the last),
// write a segment of the cable
for (final ListIterator<Node<Float>> it = slab.listIterator(slab.size()); it.hasPrevious(); ) {
// Assign an id to the node of the slab
final Node<Float> seg = it.previous();
// Write the segment
toPoint(seg, fp, aff, zScale);
writeCableSegment(w, fp, nextSegmentId, parentId, parentCoords, sCableId);
// Inspect and collect synapses originating at this node
if (null != pre) collectConnectors(seg, t, fp, nextSegmentId, pre, post);
// Prepare next segment in the cable
parentId = nextSegmentId;
nextSegmentId += 1;
parentCoords = null; // is used only for the first node
}
// Record the branch node, to be used for filling in "distal" fields
if (node.getChildrenCount() > 1) {
nodeIds.put(node, parentId); // parentId is the last used nextId, which is the id of node
final float[] fpCopy = new float[4];
toPoint(node, fpCopy, aff, zScale);
nodeCoords.put(node, fpCopy);
}
// Prepare next slab or cable
cableId += 1;
}
w.write(" </segments>\n");
// Define the nature of each cable
// Each cable requires a unique name
w.write(" <cables xmlns=\"http://morphml.org/morphml/schema\">\n");
w.write(" <cable id=\"0\" name=\"Soma\">\n <meta:group>soma_group</meta:group>\n </cable>\n");
for (long i=1; i<cableId; i++) {
final String sid = Long.toString(i);
w.write(" <cable id=\""); w.write(sid);
w.write("\" name=\""); w.write(sid);
if (somaCables.contains(i)) w.write("\" fract_along_parent=\"0.5");
else w.write("\" fract_along_parent=\"1.0"); // child segments start at the end of the segment
w.write("\">\n <meta:group>arbor_group</meta:group>\n </cable>\n");
}
w.write(" </cables>\n</cell>\n");
}
}