package org.sana.android.procedure;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.sana.android.activity.EducationResourceList;
import org.sana.android.content.core.ObservationWrapper;
import org.sana.android.db.PatientValidator;
import org.sana.android.media.EducationResource;
import org.sana.android.media.EducationResource.Audience;
import org.sana.android.procedure.ProcedureElement.ElementType;
import org.sana.android.procedure.branching.Criteria;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
* ProcedurePage the the object corresponding to a single "page" in a Sana
* procedure.
* <p/>
* Each ProcedurePage can contain several elements, although the Sana style
* recommendation is to use just one element per page. Each page is defined as
* an XML node in a procedure description.
* <p/>
* ProcedurePages may have criteria that must be true in order for the procedure
* runner to display the page. This is stored in the ProcedurePage object. See
* documentation on Criteria for more info about how Criteria work.
*
* @author Sana Development Team
*/
public class ProcedurePage {
public static final String TAG = ProcedurePage.class.getSimpleName();
private View cachedView;
private Context cachedContext;
List<ProcedureElement> elements;
Procedure procedure;
Criteria criteria;
String label = null;
/**
* Constructor for ProcedurePage if no entry criteria are desired for the
* page (the page will always display).
*
* @param elements a list of displayable elements
*/
public ProcedurePage(List<ProcedureElement> elements) {
this.elements = elements;
this.criteria = new Criteria();
}
/**
* Logs the list of elements on this page.
*/
public void listElements() {
Log.i(TAG, "listing all element types on this page");
for (int i=0; i<elements.size(); i++) {
Log.i(TAG, elements.get(i).getId());
}
}
// clears the cached view
void clearCachedView() {
for (ProcedureElement pe : elements) {
pe.clearCachedView();
}
}
/**
* Constructor for ProcedurePage where criteria will determine whether
* elements are displayed.
*
* @param elements a list of displayable elements
* @param criteria A set of logic conditions for determining visibility.
*/
public ProcedurePage(List<ProcedureElement> elements, Criteria criteria) {
this.elements = elements;
this.criteria = criteria;
}
public String getLabel(){
return label;
}
public void setLabel(String label){
this.label = label;
}
/**
* Returns the value of the answer attribute for a contained element.
*
* @param key an element id.
* @return the value of the element answer.
*/
public String getElementValue(String key) {
String value = "";
for (ProcedureElement el:elements) {
if (el.getId().compareTo(key) == 0) {
value = el.getAnswer();
break;
}
}
return value;
}
/**
* Sets the parent procedure for this object and all child elements.
*
* @param procedure the new parent.
*/
public void setProcedure(Procedure procedure) {
this.procedure = procedure;
for(ProcedureElement pe : elements) {
pe.setProcedure(procedure);
}
}
// tests whether page has special elements that need further action
private String[] specialElements = {"patientId",
"patientFirstName", "patientLastName", "patientBirthdateDay",
"patientBirthdateMonth", "patientBirthdateYear", "patientGender"};
/**
* @return true if one or more elements require non standard processing
*/
public boolean hasSpecialElement() {
for (int i=0; i<elements.size(); i++) {
for (int j=0; j<specialElements.length; j++) {
if (elements.get(i).id.equals(specialElements[j])) {
return true;
}
}
}
return false;
}
/**
* @return the element which holds the patient id if it exists.
*/
public PatientIdElement getPatientIdElement() {
PatientIdElement patientid = null;
for (int i=0; i<elements.size(); i++) {
if (elements.get(i).getType().equals(ElementType.PATIENT_ID)) {
patientid = (PatientIdElement)elements.get(i);
break;
}
}
return patientid;
}
//TODO rename to getElementById
/**
* Returns a list of all the elements on the page with a specified
* ElementType
*
* @param type the element type to match.
*/
public ProcedureElement getElementByType(String type) {
ProcedureElement p = null;
List<ProcedureElement> els = elements;
for (int i=0; i<els.size(); i++) {
if (els.get(i).getId().equals(type)) {
p = els.get(i);
}
}
return p;
}
public ProcedureElement getElementById(String id) {
ProcedureElement p = null;
for (ProcedureElement el:elements) {
if (el.getId().compareTo(id) == 0) {
p = el;
break;
}
}
return p;
}
public List<ProcedureElement> getElements(){
return elements;
}
public boolean hasElementWithId(String id) {
boolean result = false;
for (ProcedureElement el:elements) {
if (el.getId().compareTo(id) == 0) {
result = true;
break;
}
}
return result;
}
public String elementWithConcept(String concept) {
String result = null;
for (ProcedureElement el:elements) {
String compare = concept.replace("_"," ").toUpperCase();
if (el.getConcept().compareToIgnoreCase(compare) == 0) {
result = el.getId();
break;
}
}
return result;
}
public List<String> getConcepts(){
List<String> result = new ArrayList<String>();
for (ProcedureElement el:elements) {
result.add(el.getConcept());
}
return result;
}
/**
* @return A list of elements which require non-standard processing.
*/
public List<ProcedureElement> getSpecialElements() {
List<ProcedureElement> els = new ArrayList<ProcedureElement>();
for (ProcedureElement el : elements) {
els.add(el);
}
return els;
}
/**
* Sets the answer value of a specific element.
*
* @param key the element id attribute.
* @param value the new answer value.
*/
public void setElementValue(String key, String value) {
for (int i=0; i<elements.size(); i++) {
ProcedureElement e = elements.get(i);
if (e.id.equals(key)) {
e.setAnswer(value);
return;
}
}
}
/**
* Inspects a ProcedureElement to determine if it needs non-standard
* processing.
* @param e the element to inspect.
* @return true if the element reequires non-standard processing.
*/
public boolean isSpecialElement(ProcedureElement e) {
for (int i=0; i<specialElements.length; i++) {
if (e.id.equals(specialElements[i])) {
return true;
}
}
return false;
}
/**
* Validates all child elements
*
* @return true if all children are valid.
* @throws ValidationError
*/
public boolean validate() throws ValidationError {
for (ProcedureElement el : elements) {
if (!el.validate()) {
return false;
}
}
if (!PatientValidator.validate(procedure, procedure.getPatientInfo())) {
return false;
}
return true;
}
/**
* Tests whether the criteria are currently met to display this page, given
* user selections thus far.
*/
public boolean shouldDisplay() {
return criteria.criteriaMet();
}
public boolean displayForeground(){
boolean show = false;
for(ProcedureElement el: elements){
show = el.isViewActive() || show;
}
return shouldDisplay() && show;
}
/**
* Maps all of the child elements to a map using their id as a key.
* @param elementMap The map to place the elements into.
*/
public void populateElements(HashMap<String, ProcedureElement> elementMap) {
for (ProcedureElement e : elements) {
elementMap.put(e.getId(), e);
}
}
/**
* @return a new map of all child elements with their id as a key.
*/
public HashMap<String, ProcedureElement> getElementMap() {
HashMap<String, ProcedureElement> ret =
new HashMap<String, ProcedureElement>();
populateElements(ret);
return ret;
}
/**
* Plays an entry audio prompt for each child element.
*/
public void playFirstPrompt() {
for (ProcedureElement e : elements) {
if (e.hasAudioPrompt()) {
e.playAudioPrompt();
return;
}
}
}
/**
* @return The question attribute of the first element.
*/
public String getSummary() {
if(!TextUtils.isEmpty(label)) {
return label;
} else if (!elements.isEmpty())
return elements.get(0).getQuestion();
return "";
}
/**
* Returns a view of elements in this procedure.
* @param c The application Context.
* @return
*/
public View toView(Context c) {
if(cachedView == null || cachedContext != c) {
cachedContext = c;
cachedView = createView(c);
}
return cachedView;
}
/**
* Constructs a new Intent which will launch the EducationResourceList with
* a selection of the media available for the elements on this page.
* @param audience CHWs or patients.
* @return
*/
public Intent educationResources(Audience audience){
List<String> ids = new ArrayList<String>();
for(ProcedureElement pe: getElementMap().values()){
switch(pe.getType()){
case INVALID:
break;
default:
String rawStr = pe.getConcept() + pe.getQuestion();
ids.add(EducationResource.toId(rawStr));
}
}
Intent intent = EducationResourceList.getIntent(ids, audience);
return intent;
}
/** A scrollable view of the elements in this procedure */
private View createView(Context c) {
// ll contains scroll contains ill
ScrollView scroll = new ScrollView(c);
LinearLayout ll = new LinearLayout(c);
LinearLayout ill = new LinearLayout(c);
ll.setOrientation(LinearLayout.VERTICAL);
ill.setOrientation(LinearLayout.VERTICAL);
List<View> visibleElements = new ArrayList<View>();
for (ProcedureElement e : elements) {
View v = e.toView(c);
if (e.getType() != ElementType.HIDDEN) {
visibleElements.add(v);
}
}
int size = visibleElements.size();
float weight = 1.0f / ((size > 0)? size: 1);
// Only add and set layout parameters
for(View v: visibleElements){
LinearLayout subll = new LinearLayout(c);
subll.addView(v);
subll.setGravity(Gravity.CENTER);
ill.addView(subll, new LinearLayout.LayoutParams(-1, -1, weight));
}
ill.setWeightSum(1.0f);
scroll.addView(ill);
ll.addView(scroll);
ll.setGravity(Gravity.CENTER);
return ll;
}
/** Creates an XML description of the page and its elements. */
public String toXML() {
StringBuilder sb = new StringBuilder();
buildXML(sb);
return sb.toString();
}
/**
* Writes a string representation of this object to a StringBuilder
* @param sb the builder to write to.
*/
public void buildXML(StringBuilder sb) {
Log.i(TAG, "ProcedurePage.toXML()");
sb.append("<Page>\n");
for (ProcedureElement e : elements) {
e.buildXML(sb);
}
sb.append("</Page>\n");
}
/**
* Restores the state of a procedure by loading a collection of answers.
* @param answersMap A map of previously collected answers.
*/
public void restoreAnswers(Map<String,String> answersMap) {
for(ProcedureElement s : elements) {
Log.d(TAG, "...checking for id=" + s.getId());
if(answersMap.containsKey(s.getId())) {
Log.d(TAG, "...setting answer" + answersMap.get(s.getId()));
s.setAnswer(answersMap.get(s.getId()));
}
}
}
/**
* Returns a map of the child elements to answer attribute values.
* @return a new answer map.
*/
public Map<String,String> toAnswers() {
HashMap<String,String> answers = new HashMap<String,String>();
populateAnswers(answers);
return answers;
}
/**
* Restores the state of a procedure by loading a collection of answers.
* @param answers A map of previously collected answers.
*/
public void populateAnswers(Map<String,String> answers) {
for(ProcedureElement s : elements) {
answers.put(s.getId(), s.getAnswer());
}
}
/**
* Produces a new map of element properties to their ids.
*
* @return a dictionary mapping Element ids to a dictionary containing the
* properties for each Element
*/
public Map<String,Map<String,String>> toElementMap() {
HashMap<String,Map<String,String>> elementMap =
new HashMap<String,Map<String,String>>();
populateElementMap(elementMap);
return elementMap;
}
/**
* Fills in the attributes for an element into a mapping of the element id
* to its attributes.
* @param elementMap map to add to.
*/
public void populateElementMap(Map<String,Map<String,String>> elementMap) {
for(ProcedureElement s : elements) {
Map<String,String> submap = new HashMap<String,String>();
submap.put("question", s.getQuestion());
submap.put("answer", s.getAnswer());
submap.put("type", s.getType().toString());
submap.put("concept", s.getConcept());
elementMap.put(s.getId(), submap);
}
}
public List<Intent> hiddenActions(){
List<Intent> actions = new ArrayList<Intent>();
for(ProcedureElement s : elements) {
if(s.getType() == ElementType.HIDDEN && !TextUtils.isEmpty(s.action)){
HiddenElement h = (HiddenElement) s;
Intent intent = h.getIntent();
if(intent != null) actions.add(intent);
}
}
return actions;
}
public boolean hasHiddenActions(){
boolean actionable = false;
for(ProcedureElement s : elements){
actionable = actionable ||(s.getType() == ElementType.HIDDEN && !TextUtils.isEmpty(s.action));
}
return actionable;
}
/**
* Create a ProcedurePage from a node in an XML procedure description.
*/
public static ProcedurePage fromXML(Node node,
HashMap<String, ProcedureElement> elts) throws ProcedureParseException
{
//Log.i(TAG, "ProcedurePage.fromXML(" + node.toString() + ")");
if (!node.getNodeName().equals("Page")) {
throw new ProcedureParseException("ProcedurePage got NodeName "
+ node.getNodeName());
}
List<ProcedureElement> elements = new ArrayList<ProcedureElement>();
Criteria criteria = new Criteria();
NodeList children = node.getChildNodes();
boolean showIfAlreadyExists = false;
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeName().equals("Element")) {
elements.add(ProcedureElement.createElementfromXML(child));
} else if (child.getNodeName().equals("ShowIf")) {
//Log.i(TAG, "Page has ShowIf - creating Criteria");
if (showIfAlreadyExists)
throw new ProcedureParseException(
"More than one ShowIf statement!");
criteria = Criteria.fromXML(child, elts);
showIfAlreadyExists = true;
}
}
ProcedurePage pp = new ProcedurePage(elements, criteria);
return pp;
}
}