/* This Java package, org.das2.qstream is part of the Autoplot application * * Copyright (C) 2018 Chris Piker * * Autoplot is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License version 2 as published by the Free * Software Foundation, with the Classpath exception below. * * 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 version 2 * containing the Classpath exception clause along with this library; if not, write * to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * * Classpath Exception * ------------------- * The copyright holders designate this particular java package as subject to the * "Classpath" exception as provided here. * * Linking this package statically or dynamically with other modules is making a * combined work based on this package. Thus, the terms and conditions of the GNU * General Public License cover the whole combination. * * As a special exception, the copyright holders of this package give you * permission to link this package with independent modules to produce an * application, regardless of the license terms of these independent modules, and * to copy and distribute the resulting application under terms of your choice, * provided that you also meet, for each independent module, the terms and * conditions of the license of that module. An independent module is a module * which is not derived from or based on this package. If you modify this package, * you may extend this exception to your version of the package, but you are not * obligated to do so. If you do not wish to do so, delete this exception * statement from your version. */ package org.das2.qstream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.das2.datum.Datum; import org.das2.datum.DatumRange; import org.das2.datum.LoggerManager; import org.das2.datum.TimeLocationUnits; import org.das2.datum.Units; import org.das2.qds.BundleDataSet; import org.das2.qds.DataSetOps; import org.das2.qds.QDataSet; import org.das2.qds.SemanticOps; import org.w3c.dom.Document; import org.w3c.dom.Element; // In general QDataSet conglomerations have the following possible structure // though many multi-element items may collapse to a single item: // // ds is maybe list of 1-N datasets (join) // | // join // | // |- ds is maybe a list of 1-N datasets (bundle) // | // bundle // | // |- ds maybe has dependencies and stats (regular) // | // dependency, statistic or plane // | // |- ds Support datasets // // // So where do we look for fundamental items such as the xTagWidth? Well // They can be anywhere and you have to crawl the hierarchy to find out. Also // different paths down the hierarchy may lead to different answers. /** Write QDataSets that vary over at most 2 independent variables as a Das2 * stream. * Since Das2 streams can have at most 2 independent variables, higher * dimensional (not necessarily higher rank) QDataSets can't be written using * this code. Streams that can be written have the following plane structure * * X Y [Y Y Y Y ... ] * X YScan [YScan YScan YScan ... ] * X Y Z [Z Z Z Z ... ] * * All binary output is streamed in machine-native order. Since datasets written * on one architecture are most likely to be read on the same architecture this * choice causes the least amount of byte swapping. * * This is a direct serialization of QDataSet and does not require any legacy * das2 dataset classes such as VectorDataset or TableDataset. */ public class QdsToDas22 extends QdsToD2sStream { private static final Logger log = LoggerManager.getLogger("qstream"); // A das2 limitation, not an arbitrary choice static final int MAX_HDRS = 100; // List of transmitted packet hdrs, index is the packet ID. List lHdrsSent = new ArrayList<>(); public QdsToDas22(){ super(); } public QdsToDas22(int genSigDigits, int fracSecDigits){ super(genSigDigits, fracSecDigits); } /** Determine if a given dataset be serialized as a das2.2 stream * @param qds The dataset to write * @return true if this dataset can be serialized as a das2.2 stream, false * otherwise */ @Override public boolean canWrite(QDataSet qds){ // Joins are typically just appends. We can use different packet types so // that's okay as long as we don't run out of packet IDs int nTypes = 0; if(SemanticOps.isJoin(qds)){ for(int i = 0; i < qds.length(); ++i){ if(!_canWriteNonJoin(DataSetOps.slice0(qds, i ))) return false; if(nTypes++ > 99) return false; } return true; } return _canWriteNonJoin(qds); } /** Write a QDataSet as a das2.2 stream * * To test whether it looks like this code could stream a dataset use the * canWrite() function. This function may be called multiple times to add * additional data to the stream. If a compatible header has already been * emitted for a particular dataset it is not re-sent. * * @param qds The dataset to write, may have join's bundles etc. but no * rank 3 or higher component datasets. * * @param os an open output stream, which is not closed by this code. * * @return true if the entire dataset could be written, false otherwise. * IO failures do not return false, they throw. False simply * means that this code does not know how (or can't) stream the * given dataset. Since deeper inspection occurs when actually * writing the data then when testing, false may be returned even * if canWrite() returned true. * @throws java.io.IOException */ @Override public boolean write(QDataSet qds, OutputStream os) throws IOException { if(! canWrite(qds)) return false; // Try not to create invalid output String sPktHdr; List lHdrsToSend = new ArrayList<>(); Document doc; // XML document if(lHdrsSent.isEmpty()){ if((doc = _makeStreamHdr(qds)) == null) return false; lHdrsToSend.add( xmlDocToStr(doc) ); } // Take advantage of the fact that we have all the data here to place all // headers first. This way someone can use a text editor to inspect the // top of the stream, even for binary streams PacketXferInfo pxi; List lPi = new ArrayList<>(); if(SemanticOps.isJoin(qds)){ for(int i = 0; i < qds.length(); ++i){ QDataSet ds = DataSetOps.slice0(qds, i); if((pxi = _makePktXferInfo(ds)) == null) return false; else lPi.add(pxi); sPktHdr = xmlDocToStr(pxi.doc); if(!lHdrsSent.contains(sPktHdr)) lHdrsToSend.add(sPktHdr); } } else{ if((pxi = _makePktXferInfo(qds)) == null) return false; else lPi.add(pxi); sPktHdr = xmlDocToStr(pxi.doc); if(!lHdrsSent.contains(sPktHdr)) lHdrsToSend.add(sPktHdr); } for(int i = 0; i < lHdrsToSend.size(); ++i){ int iPktId = lHdrsSent.size(); // So zero is always stream header if(iPktId >= MAX_HDRS) return false; writeHeader(os, FIXED_PKT_TAGS, iPktId, lHdrsToSend.get(i)); lHdrsSent.add(lHdrsToSend.get(i)); } // Now write the data, find out packet ID by our header value int iPktId; for(PacketXferInfo pktinfo: lPi){ sPktHdr = xmlDocToStr(pktinfo.doc); iPktId = lHdrsSent.indexOf(sPktHdr); writeData(os, FIXED_PKT_TAGS, iPktId, pktinfo); } return true; } // Top level helper functions and structures ////////////////////////////// boolean _canWriteNonJoin(QDataSet qds) { // Bundles are used all over the place. They just represent items that // are covarying in the depend coordinates. To determine this, bust the // bundle apart and track dependencies. QDataSet dsDep; QDataSet dsDep0 = null; QDataSet dsDep1 = null; if(qds instanceof BundleDataSet){ for (int i = 0; i < qds.length(0); ++i) { QDataSet ds = ((BundleDataSet)qds).unbundle(i); // Ain't gonna handle bundles of bundles today if(ds instanceof BundleDataSet) return false; // Can't do rank 3+ in das2.2 if (ds.rank() > 2) return false; if (ds.rank() > 1) { // Blind cast, is there any way to check this first? dsDep = (QDataSet) ds.property(QDataSet.DEPEND_1); if (dsDep != null) { if (dsDep1 == null) { dsDep1 = dsDep; log.log(Level.FINE, "dsDep1: {0}", dsDep1); } else { // More than one dep1 running around if (dsDep != dsDep1) { return false; } } } // For now no one has come up with a concise way to say that // one yscan is the "frequency" set for a second yscan, we should // do this but for now the answer is no, can't stream it if (dsDep != null && dsDep.rank() > 1) { return false; } } if ((dsDep = (QDataSet) ds.property(QDataSet.DEPEND_0)) != null) { if (dsDep0 == null) { dsDep0 = dsDep; log.log(Level.FINE, "dsDep0: {0}", dsDep0); } else { // More than one dep0 running around if (dsDep != dsDep0) { return false; } } } } return true; // Bundle looks representable } // Not a bundle, just need to have a low enough rank for the main datasets // and the physical value index tags if (qds.rank() > 2) return false; if ((dsDep = (QDataSet) qds.property(QDataSet.DEPEND_1)) != null) { if (dsDep.rank() > 1) return false; } // Ignore the following cases, since I don't know what they mean String[] lTmp = {QDataSet.BUNDLE_0, QDataSet.BUNDLE_2, QDataSet.BUNDLE_3}; for (String s : lTmp) { if (qds.property(s) != null) { return false; } } return true; // We ran the gauntlet } //////////////////////////////////////////////////////////////////////////// // Make header for join, // For non-bundle datasets a lot of the properties can be placed in the // stream header and it's common to do so. I am not using any of the // defined strings from org.das2.DataSet since I'm assuming that package // is going away. Document _makeStreamHdr(QDataSet qds) { Document doc = newXmlDoc(); Element stream = doc.createElement("stream"); stream.setAttribute("version", FORMAT_2_2); Element props = doc.createElement("properties"); int nProps = 0; nProps += addStrProp(props, qds, QDataSet.TITLE, "title"); nProps += addStrProp(props, qds, QDataSet.DESCRIPTION, "summary"); nProps += addStrProp(props, qds, QDataSet.RENDER_TYPE, "renderer"); String sAxis = getQdsAxis(qds); if(sAxis == null) return null; nProps += _addSimpleProps(props, qds, sAxis); // If the user_properties are present, add them in Map dUser; dUser = (Map)qds.property(QDataSet.USER_PROPERTIES); boolean bStripDot = _stripDotProps(qds); if(dUser != null) nProps += addPropsFromMap(props, dUser, bStripDot); // If the ISTP-CDF metadata weren't so long, we'd include these as well // maybe a Das 2.3 sub tag is appropriate for them, ignore for now... if(nProps > 0) stream.appendChild(props); doc.appendChild(stream); return doc; } /////////////////////////////////////////////////////////////////////////// // Make header for bundle and transfer info in parallel // // For each member of the bundle // // 1. Get it's depend0, this is the X axis, if it's not the first make sure // that the new depend0 is not different from the first one. // // 2. If the member has no depend0 then it is an extra X axis, place it // after any X that arises as a depend 0 // // 3. If it has a depend1, assume it's a , if not it's a . // // branch // // a. See if depend1 has a depend0, we don't have a way to save these // at this time. // // b. See if it has a plane_0, this would mean it's a 4-D dataset, // we don't have a way to deal with those at this time. // // c. See if the depend1's can be saved as a sequence, if not save // as ytags // // branch // // a. See if it has a plane_0, if so plane_0 is a item. I don't // think there is a pattern established yet for multi 's but check // that is not a bundle. // // 4. If it has a BIN_MAX, BIN_MIN, DELTA_PLUS, DELTA_MINUS properties. // all these cause the creation of extra or with the same // SOURCE and (if needed yTags) properties PacketXferInfo _makePktXferInfo(QDataSet qds) throws IOException { List lDsXfer = new ArrayList<>(); Document doc = newXmlDoc(); Element elPkt = doc.createElement("packet"); doc.appendChild(elPkt); assert(!SemanticOps.isJoin(qds)); //Don't call this for join datasets QDataSet dep0; // We go through the list of datasets multiple times, picking out the // items we care about. In each stage lDsToRead is the list to process, // lDsRemain are the remaining items List lDsToRead = new ArrayList<>(); List lDsRemain = new ArrayList<>(); if(qds instanceof BundleDataSet){ // Primary handling: Maybe the bundle's depend_0 is if( (dep0 = (QDataSet) qds.property(QDataSet.DEPEND_0)) != null) _addPhysicalDimension(elPkt, lDsXfer, "x", dep0); for(int i = 0; i < qds.length(0); ++i) lDsToRead.add( ((BundleDataSet)qds).unbundle(i) ); } else{ lDsToRead.add(qds); } // Handle planes for(QDataSet ds: lDsToRead){ // Maybe the bundle's members depend_0 is if( (dep0 = (QDataSet) ds.property(QDataSet.DEPEND_0)) != null){ if(lDsXfer.isEmpty()){ _addPhysicalDimension(elPkt, lDsXfer, "x", dep0); } else{ if(dep0 != lDsXfer.get(0).qds){ log.warning("Multiple independent depend_0 datasets in bundle"); return null; } } lDsRemain.add(ds); // Used the depend0, but keep top level dataset } else{ // Rank 1 with no depend 0 is an plane if(lDsXfer.isEmpty() && (ds.rank() == 1)) _addPhysicalDimension(elPkt, lDsXfer, "x", ds); } } // Handle planes, these are always rank 1 and have a depend 0, if // you see a PLANE_0 save it out as a Z plane. int nNewArrays; lDsToRead = lDsRemain; lDsRemain = new ArrayList<>(); QDataSet dsZ = null; QDataSet dsSubZ; for(QDataSet ds: lDsToRead){ if(ds.rank() > 1){ lDsRemain.add(ds); continue; } // assert(ds.property(QDataSet.DEPEND_0) != null); // Once we hit a Z plane we're done with y's and yscans if(dsZ != null){ log.warning("Multiple Y planes encountered in X,Y,Z dataset"); return null; } nNewArrays = _addPhysicalDimension(elPkt, lDsXfer, "y", ds); if(nNewArrays == 0) return null; if( (dsZ = (QDataSet) ds.property(QDataSet.PLANE_0)) != null){ // If plane0 is a bundle we have multiple Z's if(SemanticOps.isBundle(dsZ)){ for(int i = 0; i < dsZ.length(); ++i){ dsSubZ = DataSetOps.slice0(dsZ, i); nNewArrays = _addPhysicalDimension(elPkt, lDsXfer, "z", dsSubZ); if(nNewArrays == 0) return null; } } else{ nNewArrays = _addPhysicalDimension(elPkt, lDsXfer, "z", dsZ);; if(nNewArrays == 0) return null; } } } // Handle planes. lDsToRead = lDsRemain; QDataSet dsYTags = null; QDataSet dep1; for(QDataSet ds: lDsToRead){ if(ds.rank() > 2){ assert(false); return null; } if(dsZ != null){ log.warning("YScan planes not allowed in the same packets as Z planes"); return null; } if( (dep1 = (QDataSet)ds.property(QDataSet.DEPEND_1)) != null){ if(dsYTags == null){ dsYTags = dep1; } else{ if(dsYTags != dep1){ log.warning("Independent Y values for different rank-2 " + " datasets in the same bundle"); return null; } } } nNewArrays = _addPhysicalDimension(elPkt, lDsXfer, "yscan", ds); if(nNewArrays < 1) return null; } return new PacketXferInfo(doc, lDsXfer); /* Headers and data xfer */ } /** Add a new phys-dim to the packet header and record the transfer info for the * corresponding data packets * * @param elPkt * @param lXfer * @param sAxis - one of "x", "y", "z" or "yscan" * @param ds * @return * @throws IOException */ int _addPhysicalDimension( Element elPkt, List lXfer, String sAxis, QDataSet ds ) throws IOException { int nArrays = 0; Document doc = elPkt.getOwnerDocument(); Element el = doc.createElement(sAxis); elPkt.appendChild(el); nArrays++; QdsXferInfo xfer = new QdsXferInfo(ds, bBinary, nSigDigit, nSecDigit); lXfer.add(xfer); String sName = _getName(elPkt, ds, sAxis); el.setAttribute("name", sName); el.setAttribute("type", _makeTypeFromXfer(xfer)); Units units = (Units)ds.property(QDataSet.UNITS); String sTag = sAxis.equals("yscan") ? "zUnits" : "units"; el.setAttribute(sTag, units != null ? units.toString() : ""); // Now handle the properties Element elProps = doc.createElement("properties"); int nProps = 0; if(sAxis.equals("yscan")){ nProps += _addSimpleProps(elProps, ds, "z"); nProps += _yTagsNProps(ds, el, elProps); // Gets Y-Tags Attribs } else{ nProps += _addSimpleProps(elProps, ds, sAxis); } if(nProps > 0) el.appendChild(elProps); // Look to see if we are going to be adding in any stats planes QDataSet dsStats; QdsXferInfo xferStats; Element elStats; String sErr = "Statistics dataset is a different rank than the average dataset"; for(String sProp: aStdPlaneProps){ if((dsStats = (QDataSet)ds.property(sProp)) != null){ if(dsStats.rank() != ds.rank()){ log.warning(sErr); continue; } xferStats = new QdsXferInfo(dsStats, bBinary, nSigDigit, nSecDigit); elStats = doc.createElement(sAxis); elPkt.appendChild(elStats); lXfer.add(xferStats); nArrays++; Units unitsStats = (Units)ds.property(QDataSet.UNITS); elStats.setAttribute("name", sName + "." + _statsName(sProp)); sTag = sAxis.equals("yscan") ? "zUnits" : "units"; elStats.setAttribute(sTag, unitsStats != null ? unitsStats.toString() : ""); elStats.setAttribute("type", _makeTypeFromXfer(xferStats)); // Now handle the properties Element elStatsProps = doc.createElement("properties"); elStatsProps.setAttribute("source", sName); elStatsProps.setAttribute("operation", sProp); if(sAxis.equals("yscan")){ _addSimpleProps(elStatsProps, dsStats, "z"); _yTagsNProps(dsStats, elStats, elStatsProps); } else{ _addSimpleProps(elStatsProps, dsStats, sAxis); } elStats.appendChild(elStatsProps); } } return nArrays; } String _getName(Element elPkt, QDataSet ds, String sPlane) { // Insure we have a name: ds.NAME -> Units -> just number // If the name is empty, make one up based on the units if you can String sName = (String)ds.property(QDataSet.NAME); if(sName != null) return sName; Units units = (Units)ds.property(QDataSet.UNITS); sName = makeNameFromUnits(units); if(sName.length() >= 0) return sName; int n = elPkt.getElementsByTagName(sPlane).getLength(); return String.format("%s_%d", sPlane.toUpperCase(), n); } String _makeTypeFromXfer(QdsXferInfo xfer) throws IOException { String sName = xfer.name(); int nSz = xfer.size(); String sReal = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? "little_endian_real" : "sun_real"; switch(sName){ case "double": return String.format("%s8", sReal); case "float": return String.format("%s4", sReal); case "ascii": return String.format("ascii%d", nSz); case "time": return String.format("time%d", nSz); default: // TODO: What kind of exception should I throw here? throw new IOException(String.format( "das2.2 streams cannot transmit data values of type %s%d", sName, nSz )); } } // List of properties that should generate a plane static final String[] aStdPlaneProps = { QDataSet.BIN_MIN, QDataSet.BIN_MAX, QDataSet.BIN_MINUS, QDataSet.BIN_PLUS }; static String _statsName(String sProp){ switch(sProp){ case QDataSet.BIN_MIN: return "min"; case QDataSet.BIN_MAX: return "max"; case QDataSet.BIN_MINUS: return "min"; case QDataSet.BIN_PLUS: return "max"; default: return "unknown"; } } /** Add the y-tags into a yscan and it's properties into the plane props * * @param ds - The dataset, will look for it's depend 1 * @param el - The element to get the yTags (or yTagMin, Interval) * @param elProps - The element to get the y properties * @return The number of properties added */ int _yTagsNProps(QDataSet ds, Element el, Element elProps) throws IOException { int nItems = ds.length(0); el.setAttribute("nitems", String.format("%d", nItems)); QDataSet dsYTags = (QDataSet)ds.property(QDataSet.DEPEND_1); // If no yTags, then just set min == 0 and interval to 1.0. if(dsYTags == null){ el.setAttribute("yTagMin", "0.0"); el.setAttribute("yTagInterval", "1.0"); return 0; } if(dsYTags.rank() != 1) throw new IOException( "das2.2 YTags must be rank 1. (Hint: Dataset may be exportable using das2.3/basic)" ); // If ds is actually at 1-D sequence, just save that instead of all the // tags. Sequence1D seq = getSequenceRank1(dsYTags, 1e-4); if(seq != null){ el.setAttribute("yTagMin", seq.sMinval); el.setAttribute("yTagInterval", seq.sInterval); } else{ String sFmt = String.format("%%.%de", nSigDigit); StringBuilder sb = new StringBuilder(); sb.append(String.format(sFmt, dsYTags.value(0))); for(int i = 1; i < dsYTags.length(); ++i) sb.append(",").append(String.format(sFmt, dsYTags.value(i))); el.setAttribute("yTags", sb.toString()); } Units yunits = (Units)dsYTags.property(QDataSet.UNITS); if(yunits != null) el.setAttribute("yUnits", yunits.toString()); else el.setAttribute("yUnits", ""); return _addSimpleProps(elProps, dsYTags, "y"); } int addBoolProp(Element props, String sName, Object oValue){ String sValue = (Boolean)oValue ? "true" : "false"; props.setAttribute(sName, sValue); return 1; } int addStrProp(Element props, QDataSet qds, String qkey, String d2key){ Object oProp; if((oProp = qds.property(qkey)) != null) return addStrProp(props, d2key, oProp); return 0; } int addStrProp(Element props, String sName, Object oValue){ // Special handling for substitutions, Das2 Streams flatten metadata // so if a %{USER_PROPERTIES.thing} substitution string is being saved, // flatten it back to just %{thing} so that it works when read again String sInput = (String)oValue; Pattern p = Pattern.compile("%\\{ *USER_PROPERTIES\\."); Matcher m = p.matcher(sInput); StringBuffer sb = new StringBuffer(); while(m.find()) m.appendReplacement(sb, "%{"); m.appendTail(sb); String sOutput = sb.toString(); props.setAttribute(sName, sOutput); return 1; } int addRealProp(Element props, QDataSet qds, String qkey, String d2key){ Object oProp; if((oProp = qds.property(qkey)) != null) return addRealProp(props, d2key, oProp); return 0; } int addRealProp(Element props, String sName, Object oValue){ Number num = (Number)oValue; String sVal = String.format("%.6e", num.doubleValue()); props.setAttribute("double:"+sName, sVal); return 1; } int addDatumProp(Element props, QDataSet qds, String qkey, String d2key){ Object oProp; if((oProp = qds.property(qkey)) != null) return addDatumProp(props, d2key, oProp); return 0; } int addDatumProp(Element props, String sName, Object oValue){ Datum datum = (Datum)oValue; props.setAttribute("Datum:"+sName, datum.toString()); return 1; } int addRngProp(Element props, QDataSet qds, String sMinKey, String sMaxKey, String sUnitsKey, String d2key){ Object oMin, oMax, oUnits; oMin = qds.property(sMinKey); oMax = qds.property(sMaxKey); oUnits = qds.property(sUnitsKey); if(oMin == null || oMax == null) return 0; Number rMin = (Number)oMin; Number rMax = (Number)oMax; String sValue; if(oUnits != null){ String sUnits = ((Units)oUnits).toString(); sValue = String.format("%.6e to %.6e %s", rMin.doubleValue(), rMax.doubleValue(), sUnits); } else sValue = String.format("%.6e to %.6e", rMin.doubleValue(), rMax.doubleValue()); props.setAttribute("DatumRange:"+d2key, sValue); return 1; } int addRngProp(Element props, String sName, Object oValue){ DatumRange rng = (DatumRange)oValue; // Work around a bug in DatumRange. DatumRange can not read it's own // output. For example a time range becomes: // range.toString() => "2017-09-01 9:00 to 10:00" // But the input reader expects: // "2017-09-01T9:00 to 2017-09-01T10:00 UTC" // or it fails Units units = rng.getUnits(); String sOutput; if( units instanceof TimeLocationUnits){ Datum dmMin = rng.min(); Datum dmMax = rng.max(); sOutput = String.format( "%s to %s UTC", dmMin.toString().replaceAll("Z", ""), dmMax.toString().replaceAll("Z", "") ); } else{ sOutput = rng.toString(); } props.setAttribute("DatumRange:"+sName, sOutput); return 1; } // Get all the simple standard properties of a dataset and add these to the // attributes of a property element. Complex properties dependencies and // associated datasets are not handled here. Returns the number of props // added. int _addSimpleProps(Element props, QDataSet qds, String sAxis) { int nProps = 0; nProps += addStrProp(props, qds, QDataSet.FORMAT, sAxis + "Format"); nProps += addStrProp(props, qds, QDataSet.SCALE_TYPE, sAxis + "ScaleType"); nProps += addStrProp(props, qds, QDataSet.LABEL, sAxis + "Label"); nProps += addStrProp(props, qds, QDataSet.DESCRIPTION, sAxis + "Summary"); nProps += addRealProp(props, qds, QDataSet.FILL_VALUE, sAxis + "Fill"); nProps += addRealProp(props, qds, QDataSet.VALID_MIN, sAxis + "ValidMin"); nProps += addRealProp(props, qds, QDataSet.VALID_MAX, sAxis + "ValidMax"); nProps += addRngProp(props, qds, QDataSet.TYPICAL_MIN, QDataSet.TYPICAL_MAX, QDataSet.UNITS, sAxis + "Range"); // The cadence is an odd bird, a QDataSet with 1 item, handle special Object obj = qds.property(QDataSet.CADENCE); if(obj != null){ QDataSet dsTmp = (QDataSet)obj; Units units = (Units) dsTmp.property(QDataSet.UNITS); if(units == null) units = Units.dimensionless; Datum dtm = Datum.create(dsTmp.value(), units); props.setAttribute("Datum:"+ sAxis + "TagWidth", dtm.toString()); ++nProps; } return nProps; } // Get all the user_data properties that don't conflict with any properties // already present. These don't care about prepending x,y,z axis identifiers // to the attribute tag, though they may have them and that's okay. int addPropsFromMap(Element props, Map dMap, boolean bStripDot){ if(dMap == null) return 0; int nAdded = 0; for(Map.Entry ent: dMap.entrySet()){ String sKey = ent.getKey(); if(bStripDot && sKey.contains(".")) continue; if(props.hasAttribute(sKey)) continue; Object oVal = ent.getValue(); if(oVal instanceof Boolean) nAdded += addBoolProp(props, sKey, oVal); else if(oVal instanceof String) nAdded += addStrProp(props, sKey, oVal); else if(oVal instanceof Number) nAdded += addRealProp(props, sKey, oVal); else if(oVal instanceof Datum) nAdded += addDatumProp(props, sKey, oVal); else if(oVal instanceof DatumRange) nAdded += addRngProp(props, sKey, oVal); } return nAdded; } }