package org.redcross.openmapkit; import com.spatialdev.osm.model.OSMElement; import org.apache.commons.io.FileUtils; import org.json.JSONException; import org.json.JSONObject; import org.redcross.openmapkit.odkcollect.ODKCollectHandler; import org.redcross.openmapkit.odkcollect.tag.ODKTag; import org.redcross.openmapkit.tagswipe.TagEdit; import java.io.File; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; public class Constraints { private static Constraints instance; private JSONObject defaultConstraintsJson; private JSONObject formConstraintsJson; /** * Contains a map of constraint keys with the key value pairs of the * OSM Element conditions in which a tag view should be hidden. * * The key of the outer map is to the tag in question */ private Map<String, Map<String, String>> hideMap = new HashMap<>(); /** * This map is similar, but the key to the outer map is of tag keys * that will cause the inner Set of tag keys to be hidden. */ private Map<String, Map<String, Set<String>>> causeHideMap = new HashMap<>(); /** * Contains a map of constraint keys with the key value pairs of the * OSM Element conditions in which a tag view should be shown. * * The key of the outer map is to the tag in question. */ private Map<String, Map<String, String>> showMap = new HashMap<>(); /** * The key of the outer map is to the tag key that causes the inner * set of tag keys to be shown. */ private Map<String, Map<String, Set<String>>> causeShowMap = new HashMap<>(); private boolean active = true; public class TagAction { public Set<String> hide = new HashSet<>(); public Set<String> show = new HashSet<>(); } public static Constraints initialize() { instance = new Constraints(); return instance; } public static Constraints singleton() { return instance; } /** * If there is no default.json constraints file, we have nothing to work with, * so the constraint functionality is disabled. * * @return boolean if the constraint is active */ public boolean isActive() { return active; } public boolean tagIsNumeric(String tagKey) { return cascadeBooleanTagConstraint(tagKey, "numeric", false); } public boolean tagAllowsCustomValue(String tagKey) { return cascadeBooleanTagConstraint(tagKey, "custom_value", false); } public boolean tagIsRequired(String tagKey) { return cascadeBooleanTagConstraint(tagKey, "required", false); } public boolean tagIsSelectMultiple(String tagKey) { return cascadeBooleanTagConstraint(tagKey, "select_multiple", false); } public Set<String> requiredTagsNotMet(OSMElement osmElement) { Set<String> missingTags = new HashSet<>(); if (ODKCollectHandler.isODKCollectMode()) { Map<String, String> tags = osmElement.getTags(); Collection<ODKTag> odkTags = ODKCollectHandler.getODKCollectData().getRequiredTags(); for (ODKTag odkTag : odkTags) { String odkTagKey = odkTag.getKey(); if (cascadeBooleanTagConstraint(odkTagKey, "required", false)) { String osmTagVal = tags.get(odkTagKey); if (osmTagVal == null || osmTagVal.length() < 1) { missingTags.add(odkTagKey); } } } } return missingTags; } public String tagDefaultValue(String tagKey) { return cascadeStringTagConstraint(tagKey, "default", null); } public boolean tagShouldBeShown(String tagKey, OSMElement osmElement) { if (!isActive()) return true; // Check showMap Map<String, String> showMapMap = showMap.get(tagKey); if (showMapMap != null && showMapMap.size() > 0) { Set<String> showMapMapKeys = showMapMap.keySet(); for (String key : showMapMapKeys) { String val = showMapMap.get(key); // wildcard if (val.equals("true")) { if (osmElement.getTags().keySet().contains(key)) { // OSM Element has a tag key that is required (declared with true) // by the show_if condition for the given tag_key. return true; } } else { String osmElementTagVal = osmElement.getTags().get(key); if (osmElementTagVal != null && osmElementTagVal.equals(val)) { // OSM Element has a tag key and value that match the // show_if condition for the given tagKey. return true; } } } // OSM Element has no tags that match the show_if condition for the given tagKey. return false; } // Check hideMap Map<String, String> hideMapMap = hideMap.get(tagKey); if (hideMapMap != null && hideMapMap.size() > 0) { Set<String> hideMapMapKeys = hideMapMap.keySet(); for (String key : hideMapMapKeys) { String val = hideMapMap.get(key); // wildcard if (val.equals("true")) { if (osmElement.getTags().keySet().contains(key)) { return false; } } else { String osmElementTagVal = osmElement.getTags().get(key); if (osmElementTagVal != null && osmElementTagVal.equals(val)) { return false; } } } } // If the tag isn't mentioned in the showMap or the hideMap, then we should show it! return true; } /** * If the tag has an implicit value, it returns it. * Otherwise, it returns null. * * This is how to test if the tag should be considered implicit. * * @param tagKey * @return implicit value or null */ public String implicitVal(String tagKey) { return cascadeStringTagConstraint(tagKey, "implicit", null); } public TagAction tagAddedOrEdited(String key, String val) { TagAction tagAction = new TagAction(); if (!isActive()) return tagAction; tagAction.hide = findTagsToBeHiddenFromUpdate(key, val); tagAction.show = findTagsToBeShownFromUpdate(key, val); return tagAction; } public TagAction tagDeleted(String key) { TagAction tagAction = new TagAction(); if (!isActive()) return tagAction; tagAction.hide = findTagsToBeHiddenFromDelete(key); tagAction.show = findTagsToBeShownFromDelete(key); return tagAction; } private Constraints() { loadConstraintsJson(); buildSkipLogic(); } private void loadConstraintsJson() { try { File defaultConstraintsFile = ExternalStorage.fetchConstraintsFile("default"); String defaultConstraintsStr = FileUtils.readFileToString(defaultConstraintsFile); defaultConstraintsJson = new JSONObject(defaultConstraintsStr); } // If there is no default.json constraints file, // we just turn off Constraints functionality. catch (Exception e) { active = false; } if (!ODKCollectHandler.isODKCollectMode()) return; String formFileName = ODKCollectHandler.getODKCollectData().getFormFileName(); ExternalStorage.copyFormConstraintsFromOdk(formFileName); try { File formConstraintsFile = ExternalStorage.fetchConstraintsFile(formFileName); String formConstraintsStr = FileUtils.readFileToString(formConstraintsFile); formConstraintsJson = new JSONObject(formConstraintsStr); } catch (Exception e) { // do nothing // We typically do not need a constraints specific to a given form, // so this is normal. } } private boolean cascadeBooleanTagConstraint(String tagKey, String tagConstraint, boolean defaultVal) { boolean val = defaultVal; if (!isActive()) return val; try { JSONObject tagConstraints = defaultConstraintsJson.getJSONObject(tagKey); val = tagConstraints.getBoolean(tagConstraint); } catch (JSONException e) { // do nothing } if (formConstraintsJson != null) { try { JSONObject tagConstraints = formConstraintsJson.getJSONObject(tagKey); val = tagConstraints.getBoolean(tagConstraint); } catch (JSONException e) { // do nothing } } return val; } private String cascadeStringTagConstraint(String tagKey, String tagConstraint, String defaultVal) { String val = defaultVal; if (!isActive()) return val; try { JSONObject tagConstraints = defaultConstraintsJson.getJSONObject(tagKey); val = tagConstraints.getString(tagConstraint); } catch (JSONException e) { // do nothing } if (formConstraintsJson != null) { try { JSONObject tagConstraints = formConstraintsJson.getJSONObject(tagKey); val = tagConstraints.getString(tagConstraint); } catch (JSONException e) { // do nothing } } return val; } private void buildSkipLogic() { if (defaultConstraintsJson != null) { buildHideMaps(defaultConstraintsJson); buildShowMaps(defaultConstraintsJson); } if (formConstraintsJson != null) { buildHideMaps(formConstraintsJson); buildShowMaps(formConstraintsJson); } } private void buildHideMaps(JSONObject constraintsJson) { Iterator<String> tagKeys = constraintsJson.keys(); // iterate through main tag keys while (tagKeys.hasNext()) { String tag = tagKeys.next(); try { JSONObject constraints = constraintsJson.getJSONObject(tag); Map<String, String> hideMapMap = hideMap.get(tag); if (hideMapMap == null) { hideMapMap = new HashMap<>(); hideMap.put(tag, hideMapMap); } JSONObject hideIf = constraints.getJSONObject("hide_if"); Iterator<String> hideIfKeys = hideIf.keys(); while (hideIfKeys.hasNext()) { String hideIfKey = hideIfKeys.next(); String hideIfVal = hideIf.optString(hideIfKey); Map<String, Set<String>> causeHideMapMap = causeHideMap.get(hideIfKey); if (causeHideMapMap == null) { causeHideMapMap = new HashMap<>(); causeHideMapMap.put(hideIfVal, new HashSet<String>()); causeHideMap.put(hideIfKey, causeHideMapMap); } else if (causeHideMapMap.get(hideIfVal) == null) { causeHideMapMap.put(hideIfVal, new HashSet<String>()); } hideMapMap.put(hideIfKey, hideIfVal); causeHideMapMap.get(hideIfVal).add(tag); } } catch (JSONException e) { // do nothing } } } private void buildShowMaps(JSONObject constraintsJson) { Iterator<String> tagKeys = constraintsJson.keys(); // iterate through main tag keys while (tagKeys.hasNext()) { String tag = tagKeys.next(); try { JSONObject constraints = constraintsJson.getJSONObject(tag); Map<String, String> showMapMap = showMap.get(tag); if (showMapMap == null) { showMapMap = new HashMap<>(); showMap.put(tag, showMapMap); } JSONObject showIf = constraints.getJSONObject("show_if"); Iterator<String> showIfKeys = showIf.keys(); while (showIfKeys.hasNext()) { String showIfKey = showIfKeys.next(); String showIfVal = showIf.optString(showIfKey); Map<String, Set<String>> causeShowMapMap = causeShowMap.get(showIfKey); if (causeShowMapMap == null) { causeShowMapMap = new HashMap<>(); causeShowMapMap.put(showIfVal, new HashSet<String>()); causeShowMap.put(showIfKey, causeShowMapMap); } else if (causeShowMapMap.get(showIfVal) == null) { causeShowMapMap.put(showIfVal, new HashSet<String>()); } showMapMap.put(showIfKey, showIfVal); causeShowMapMap.get(showIfVal).add(tag); } } catch (JSONException e) { // do nothing } } } private Set<String> findTagsToBeHiddenFromUpdate(String key, String val) { Set<String> tags = new HashSet<>(); Map<String, Set<String>> causeHideMapMap = causeHideMap.get(key); if (causeHideMapMap != null) { Set<String> set = causeHideMapMap.get("true"); if (set != null) { tags.addAll(set); } set = causeHideMap.get(key).get(val); if (set != null) { tags.addAll(set); } } Map<String, Set<String>> causeShowMapMap = causeShowMap.get(key); if (causeShowMapMap != null) { Set<String> keys = causeShowMapMap.keySet(); for (String k : keys) { // We should still show the keys that should be shown by // the key wildcard and the specific new value selected. if (k.equals("true") || k.equals(val)) continue; Set<String> tagsToHide = causeShowMapMap.get(k); tags.addAll(tagsToHide); } } return tags; } private Set<String> findTagsToBeShownFromUpdate(String key, String val) { Set<String> tags = new HashSet<>(); Map<String, Set<String>> causeShowMapMap = causeShowMap.get(key); if (causeShowMapMap != null) { Set<String> set = causeShowMapMap.get("true"); if (set != null) { tags.addAll(set); } set = causeShowMap.get(key).get(val); if (set != null) { tags.addAll(set); } } return tags; } private Set<String> findTagsToBeHiddenFromDelete(String key) { Set<String> tags = new HashSet<>(); Map<String, Set<String>> allTags = causeShowMap.get(key); if (allTags != null && allTags.size() > 0) { Set<String> vals = allTags.keySet(); for (String v : vals) { tags.addAll(allTags.get(v)); } } return tags; } private Set<String> findTagsToBeShownFromDelete(String key) { Set<String> tags = new HashSet<>(); Map<String, Set<String>> allTags = causeHideMap.get(key); if (allTags != null && allTags.size() > 0) { Set<String> vals = allTags.keySet(); for (String v : vals) { tags.addAll(allTags.get(v)); } } return tags; } }