+ /*
+ * if attribute value is a map, drill down one more level to
+ * record its sub-fields
+ */
+ if (value instanceof Map<?, ?>)
+ {
+ for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
+ {
+ String[] attNames = new String[attName.length + 1];
+ System.arraycopy(attName, 0, attNames, 0, attName.length);
+ attNames[attName.length] = entry.getKey().toString();
+ addAttribute(featureType, description, entry.getValue(), attNames);
+ }
+ return;
+ }
+
+ String valueAsString = value.toString();
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts == null)
+ {
+ atts = new TreeMap<>(comparator);
+ attributes.put(featureType, atts);
+ }
+ AttributeData attData = atts.get(attName);
+ if (attData == null)
+ {
+ attData = new AttributeData();
+ atts.put(attName, attData);
+ }
+ attData.addInstance(description, valueAsString);
+ }
+
+ /**
+ * Answers the description of the given attribute for the given feature type,
+ * if known and unique, else null
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public String getDescription(String featureType, String... attName)
+ {
+ String desc = null;
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null)
+ {
+ desc = attData.getDescription();
+ }
+ }
+ return desc;
+ }
+
+ /**
+ * Answers the [min, max] value range of the given attribute for the given
+ * feature type, if known, else null. Attributes with a mixture of text and
+ * numeric values are considered text (do not return a min-max range).
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public float[] getMinMax(String featureType, String... attName)
+ {
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null && attData.hasValue)
+ {
+ return new float[] { attData.min, attData.max };
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Records the given attribute description for the given feature type
+ *
+ * @param featureType
+ * @param attName
+ * @param description
+ */
+ public void addDescription(String featureType, String description,
+ String... attName)
+ {
+ if (featureType == null || attName == null)
+ {
+ return;
+ }
+
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts == null)
+ {
+ atts = new TreeMap<>(comparator);
+ attributes.put(featureType, atts);
+ }
+ AttributeData attData = atts.get(attName);
+ if (attData == null)
+ {
+ attData = new AttributeData();
+ atts.put(attName, attData);
+ }
+ attData.addDescription(description);
+ }
+
+ /**
+ * Answers the datatype of the feature, which is one of Character, Number or
+ * Mixed (or null if not known), as discovered from values recorded.
+ *
+ * @param featureType
+ * @param attName
+ * @return
+ */
+ public Datatype getDatatype(String featureType, String... attName)
+ {
+ Map<String[], AttributeData> atts = attributes.get(featureType);
+ if (atts != null)
+ {
+ AttributeData attData = atts.get(attName);
+ if (attData != null)
+ {
+ return attData.getType();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Resets all attribute metadata
+ */
+ public void clear()
+ {
+ attributes.clear();
+ }
+
+ /**
+ * Resets attribute metadata for one feature type
+ *
+ * @param featureType
+ */
+ public void clear(String featureType)
+ {
+ Map<String[], AttributeData> map = attributes.get(featureType);
+ if (map != null)