/*-
*******************************************************************************
* Copyright (c) 2011, 2014 Diamond Light Source Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthew Gerring - initial API and implementation and/or initial documentation
*******************************************************************************/
package org.eclipse.dawnsci.hdf.object.nexus;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.TreeMap;
import org.eclipse.dawnsci.hdf.object.H5Utils;
import org.eclipse.dawnsci.hdf.object.HierarchicalDataFactory;
import org.eclipse.dawnsci.hdf.object.IFileFormatDataFile;
import org.eclipse.dawnsci.hdf.object.IHierarchicalDataFile;
import org.eclipse.january.dataset.IDataset;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import hdf.object.Attribute;
import hdf.object.Dataset;
import hdf.object.Datatype;
import hdf.object.FileFormat;
import hdf.object.Group;
import hdf.object.HObject;
import hdf.object.h5.H5Datatype;
/**
* Class used to mark groups in the hdf5 tree with nexus attributes.
*
* This is a way not to use the nexus API.
*
* @author gerring
*
*/
public class NexusUtils {
public static final String NXCLASS = "NX_class";
public static final String AXIS = "axis";
public static final String LABEL = "label";
public static final String PRIM = "primary";
public static final String SIGNAL = "signal";
public static final String UNIT = "units";
private final static Logger logger = LoggerFactory.getLogger(NexusUtils.class);
/**
* Sets the nexus attribute so that if something is looking for them,
* then they are there.
*
* @param file
* @param entry
* @param entryKey
* @throws Exception
*/
public static void setNexusAttribute(final FileFormat file,
final HObject entry,
final String entryKey) throws Exception {
setAttribute(file, entry, NXCLASS, entryKey);
}
/**
* Set any attribute on a HObject.
*
* @param file
* @param entry
* @param name
* @param entryKey
* @throws Exception
*/
public static void setAttribute(final FileFormat file,
final HObject entry,
final String name,
final String entryKey) throws Exception {
setAttribute(file, entry, name, entryKey, ATTRIBUTE_TYPE.FORCE);
}
public enum ATTRIBUTE_TYPE {
/**
* Create if necessary or overwrite
*/
FORCE,
/**
* Do not create but overwrite if exists
*/
OVERWRITE,
/**
* Do not update if exists and is equal, otherwise create
*/
NO_OVERWRITE;
}
public static void setAttribute(final FileFormat file,
final HObject entry,
final String name,
final String entryKey,
final ATTRIBUTE_TYPE type) throws Exception {
boolean attributeExist = false;
// Check if attribute is already there
@SuppressWarnings("unchecked")
final List<Object> attrList = entry.getMetadata();
if (attrList!=null) {
LOOP: for (Object object : attrList) {
if (object instanceof Attribute) {
final Attribute a = (Attribute)object;
if (name.equals(a.getName())) {
final Object oValue = a.getValue();
final String[] aValue = oValue instanceof String[] ? (String[])oValue : null;
if (aValue!=null && type==ATTRIBUTE_TYPE.NO_OVERWRITE && entryKey.equals(aValue[0])) {
return;
}
attributeExist = true;
if (type == ATTRIBUTE_TYPE.OVERWRITE) {
//Seems to be needed if you are overwriting a shorter attribute string
// to not get truncation
final long id = entry.open();
try {
entry.removeMetadata(a);
attrList.remove(a);
String[] classValue = {entryKey};
Datatype attrType = new H5Datatype(Datatype.CLASS_STRING, classValue[0].length()+1, -1, -1);
Attribute attr = new Attribute(name, attrType, new long[]{1});
attr.setValue(classValue);
entry.writeMetadata(attr);
} finally {
entry.close(id);
}
return;
}
break LOOP;
}
}
}
}
if (type==ATTRIBUTE_TYPE.OVERWRITE & !attributeExist) {
return;
}
final long id = entry.open();
try {
String[] classValue = {entryKey};
Datatype attrType = new H5Datatype(Datatype.CLASS_STRING, classValue[0].length()+1, -1, -1);
Attribute attr = new Attribute(name, attrType, new long[]{1});
attr.setValue(classValue);
file.writeAttribute(entry, attr, attributeExist);
if (entry instanceof Group) {
attrList.add(attr);
((Group)entry).writeMetadata(attrList);
} else if (entry instanceof Dataset) {
attrList.add(attr);
((Dataset)entry).writeMetadata(attrList);
}
} finally {
entry.close(id);
}
}
/**
* Does not replace the attribute if it exists
* @param file
* @param entry
* @param name
* @param value
* @throws Exception
*/
public static void setIntAttribute(final FileFormat file,
final HObject entry,
final String name,
final int value) throws Exception {
@SuppressWarnings("unchecked")
final List<Object> attrList = entry.getMetadata();
if (attrList!=null) for (Object object : attrList) {
if (object instanceof Attribute) {
final Attribute a = (Attribute)object;
if (name.equals(a.getName())) return;
}
}
final long id = entry.open();
try {
Datatype attrType = new H5Datatype(Datatype.CLASS_INTEGER, 1, -1, -1);
Attribute attr = new Attribute(name, attrType, new long[]{1});
attr.setValue(new int[]{value});
file.writeAttribute(entry, attr, false);
if (entry instanceof Group) {
attrList.add(attr);
((Group)entry).writeMetadata(attrList);
} else if (entry instanceof Dataset) {
attrList.add(attr);
((Dataset)entry).writeMetadata(attrList);
}
} finally {
entry.close(id);
}
}
public static void setDatasetAttribute(IDataset dataset, String dsPath, IHierarchicalDataFile file) throws Exception {
setDatasetAttribute(dataset, (Dataset)file.getData(dsPath), file);
}
public static void setDatasetAttribute(IDataset dataset, Dataset ds, IHierarchicalDataFile file) throws Exception {
setDatasetAttribute(((IFileFormatDataFile)file).getFileFormat(),
ds,
dataset.getName(),
H5Utils.getDatatype(dataset),
H5Utils.getLong(dataset.getShape()),
((org.eclipse.january.dataset.Dataset)dataset).getBuffer());
}
/**
* Does not replace the attribute if it exists
* @param file
* @param entry
* @param name
* @param value
* @throws Exception
*/
public static void setDatasetAttribute(final FileFormat file,
final HObject entry,
final String name,
final Datatype dtype, final long[] shape, final Object buffer) throws Exception {
@SuppressWarnings("unchecked")
final List<Object> attrList = entry.getMetadata();
if (attrList!=null) for (Object object : attrList) {
if (object instanceof Attribute) {
final Attribute a = (Attribute)object;
if (name.equals(a.getName())) return;
}
}
final long id = entry.open();
try {
Attribute attr = new Attribute(name, dtype, shape);
attr.setValue(buffer);
file.writeAttribute(entry, attr, false);
if (entry instanceof Group) {
attrList.add(attr);
((Group)entry).writeMetadata(attrList);
} else if (entry instanceof Dataset) {
attrList.add(attr);
((Dataset)entry).writeMetadata(attrList);
}
} finally {
entry.close(id);
}
}
/**
* Gets the nexus axes from the data node, if there are any there
*
* TODO Deal with label attribute?
*
* @param FileFormat - the file
* @param dataNode - the node with the signal
* @param dimension, we want the axis for 1, 2, 3 etc.
* @return
* @throws Exception especially if dims are ask for which the signal does not have.
*/
public static List<String> getAxes(final FileFormat file, final Dataset signal, int dimension) throws Exception {
final List<String> axesTmp = new ArrayList<String>(3);
final Map<Integer, String> axesMap = new TreeMap<Integer, String>();
signal.getMetadata();
if (dimension>signal.getDims().length) return null;
final long size = signal.getDims()[dimension-1];
final String parentPath = signal.getFullName().substring(0, signal.getFullName().lastIndexOf("/"));
final Group parent = (Group)file.get(parentPath);
int fakePosValue = Integer.MAX_VALUE;
final List<HObject> children = parent.getMemberList();
for (HObject hObject : children) {
final List<?> att = hObject.getMetadata();
if (!(hObject instanceof Dataset)) continue;
if (hObject.getFullName().equals(signal.getFullName())) continue;
String axis = null;
int pos = -1;
boolean isSignal = false;
for (Object object : att) {
if (object instanceof Attribute) {
Attribute attribute = (Attribute)object;
if (AXIS.equals(attribute.getName())) {
int iaxis = getAttributeIntValue(attribute);
if (iaxis<0) { // We look for comma separated string
final int[] axesDims = getAttributeIntValues(attribute);
final long[] shapeAxes = ((Dataset)hObject).getDims();
final long[] shapeData = signal.getDims();
if (axesDims!=null && axesCompatible(axesDims,shapeAxes,shapeData)) {
for (int dim : axesDims) {
if (dim == dimension) {
axis = ((Dataset)hObject).getFullName()+":"+dimension;
break;
}
}
}
}
if (iaxis == dimension) {
final long[] dims = ((Dataset)hObject).getDims();
if ((dims.length == 1 && dims[0]==size) || dims.length != 1) {
axis = ((Dataset)hObject).getFullName();
}
}
} else if (PRIM.equals(attribute.getName())) {
if (pos!=0) pos = getAttributeIntValue(attribute);
} else if (LABEL.equals(attribute.getName())) {
int labelAxis = getAttributeIntValue(attribute);
if (labelAxis == dimension) pos = 0;
} else if (SIGNAL.equals(attribute.getName())) {
isSignal = true;
axis = null;
pos = -1;
break;
}
}
}
//prioritise datasets that specify an axes (even with no primary attribute) over other datasets
if (axis != null && pos == -1) {
pos = fakePosValue--;
}
// Add any the same shape as this dimension
// providing that they are not signals
// Some nexus files set axis wrong
if (axis==null && !isSignal) {
final long[] dims = ((Dataset)hObject).getDims();
if (dims[0]==size && dims.length==1) {
axis = ((Dataset)hObject).getFullName();
}
}
if (axis!=null) {
if (pos<0) {
axesTmp.add(axis);
} else {
axesMap.put(pos, axis);
}
}
}
final List<String> axes = new ArrayList<String>(3);
if (!axesMap.isEmpty()) {
for (Integer pos : axesMap.keySet()) {
axes.add(axesMap.get(pos));
}
}
axes.addAll(axesTmp);
return axes;
}
private static boolean axesCompatible(int[] axesDims, long[] shapeAxes, long[] shapeData) {
if (axesDims == null) return false;
if (Arrays.equals(shapeAxes, shapeData)) return true;
for (int i = 0 ; i < axesDims.length; i++) {
if (shapeAxes[i] != shapeData[axesDims[i]-1]) {
return false;
}
}
return true;
}
/**
* Gets the int value or returns -1 (Can only be used for values which are not allowed to be -1!)
* @param attribute
* @return
*/
private static int getAttributeIntValue(Attribute attribute) {
final Object ob = attribute.getValue();
if (ob instanceof int[]) {
int[] ia = (int[])ob;
return ia[0];
} else if (ob instanceof String[]) {
String[] sa = (String[])ob;
try {
return Integer.parseInt(sa[0]);
} catch (Throwable ne) {
return -1;
}
}
return -1;
}
private static int[] getAttributeIntValues(Attribute attribute) {
final Object ob = attribute.getValue();
if (ob instanceof String[]) {
String[] sa = (String[])ob;
try {
final String[] axes = sa[0].split(",");
int[] ret = new int[axes.length];
for (int i = 0; i < axes.length; i++) {
ret[i] = Integer.parseInt(axes[i].trim());
}
return ret;
} catch (Throwable ne) {
return null;
}
}
return null;
}
/**
* Returns names of axes in group at same level as name passed in.
*
* This opens and safely closes a nexus file if one is not already open for
* this location.
*
* @param filePath
* @param nexusPath - path to signal dataset
* @param dimension, the dimension we want the axis for starting with 1
* @return
* @throws Exception
*/
public static List<String> getAxisNames(String filePath, String nexusPath, int dimension) throws Exception {
if (filePath==null || nexusPath==null) return null;
if (dimension<1) return null;
IHierarchicalDataFile file = null;
try {
file = HierarchicalDataFactory.getReader(filePath, true);
return file.getNexusAxesNames(nexusPath, dimension);
} finally {
if (file!=null) file.close();
}
}
/**
* Returns the attribute name of a nexus group.
*
* If the group has more than one attribute only the first is returned
*
* @param group
* @throws Exception
*/
public static String getNexusGroupAttributeValue(IHierarchicalDataFile file, String group, String name) throws Exception {
final HObject object = (HObject)file.getData(group);
for (Object ob: object.getMetadata()) {
if (ob instanceof Attribute) {
Attribute ab = (Attribute)ob;
if (ab.getName().toLowerCase().equals(name.toLowerCase())) {
Object test = ab.getValue();
if (test instanceof String[])
return ((String[])test)[0];
}
}
}
return null;
}
/**
* Breath first search of a hierarchical data file group.
*
* @param finder - IFindInNexus object, used to test a group
* @param rootGroup - the group to be searched
* @param findFirst - whether the search returns when the first object is found (quicker for single objects)
*/
public static List<String> nexusBreadthFirstSearch(IHierarchicalDataFile file, IFindInNexus finder, String rootGroup, boolean findFirst) throws Exception {
List<String> out = new ArrayList<String>();
Queue<String> queue = new LinkedList<String>();
for (String nxObject : file.memberList(rootGroup)) {
if (finder.inNexus(nxObject)) {
if (findFirst) return Arrays.asList(nxObject);
else out.add(nxObject);
}
if(file.isGroup(nxObject)) {
queue.add(nxObject);
}
}
Integer i = 0;
while (queue.size() != 0) {
String group = queue.poll();
for (String nxObject: file.memberList(group)) {
if (finder.inNexus(nxObject)) {
if (findFirst) return Arrays.asList(nxObject);
else out.add(nxObject);
}
if (file.isGroup(nxObject)) {
queue.add(nxObject);
}
i++;
}
}
if (i > 50) {
final String name = rootGroup.substring(rootGroup.lastIndexOf('/')+1);
logger.debug("This many times through loop (For node "+ name +"): " + i.toString());
}
return out;
}
}