// Copyright 2015 The Project Buendia Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at: http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distrib-
// uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// specific language governing permissions and limitations under the License.
package org.projectbuendia.client.sync;
import android.content.ContentResolver;
import android.database.Cursor;
import com.google.common.collect.ImmutableSet;
import org.projectbuendia.client.json.ConceptType;
import org.projectbuendia.client.models.Chart;
import org.projectbuendia.client.models.ChartItem;
import org.projectbuendia.client.models.ChartSection;
import org.projectbuendia.client.models.ConceptUuids;
import org.projectbuendia.client.models.Form;
import org.projectbuendia.client.models.Obs;
import org.projectbuendia.client.models.ObsRow;
import org.projectbuendia.client.models.Order;
import org.projectbuendia.client.providers.Contracts;
import org.projectbuendia.client.providers.Contracts.ChartItems;
import org.projectbuendia.client.providers.Contracts.ConceptNames;
import org.projectbuendia.client.providers.Contracts.Concepts;
import org.projectbuendia.client.providers.Contracts.Observations;
import org.projectbuendia.client.providers.Contracts.Orders;
import org.projectbuendia.client.utils.Logger;
import org.projectbuendia.client.utils.Utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
/** A helper class for retrieving and localizing data to show in patient charts. */
public class ChartDataHelper {
@Deprecated
public static final String CHART_GRID_UUID = "ea43f213-66fb-4af6-8a49-70fd6b9ce5d4";
@Deprecated
public static final String CHART_TILES_UUID = "975afbce-d4e3-4060-a25f-afcd0e5564ef";
public static final String ENGLISH_LOCALE = "en";
/** UUIDs for concepts that mean everything is normal; there is no worrying symptom. */
public static final ImmutableSet<String> NO_SYMPTOM_VALUES = ImmutableSet.of(
ConceptUuids.NO_UUID, // NO
ConceptUuids.SOLID_FOOD_UUID, // Solid food
ConceptUuids.NORMAL_UUID, // NORMAL
ConceptUuids.NONE_UUID); // None
private final ContentResolver mContentResolver;
private static final Logger LOG = Logger.create();
/** When non-null, sConceptNames and sConceptTypes contain valid data for this locale. */
private static Object sLoadingLock = new Object();
private static String sLoadedLocale;
private static Map<String, String> sConceptNames;
private static Map<String, ConceptType> sConceptTypes;
public ChartDataHelper(ContentResolver contentResolver) {
mContentResolver = checkNotNull(contentResolver);
}
/** Marks in-memory concept data out of date. Call this when concepts change in the app db. */
public static void invalidateLoadedConceptData() {
sLoadedLocale = null;
}
/** Loads concept names and types from the app db into HashMaps in memory. */
public void loadConceptData(String locale) {
synchronized (sLoadingLock) {
if (!locale.equals(sLoadedLocale)) {
sConceptNames = new HashMap<>();
try (Cursor c = mContentResolver.query(
ConceptNames.CONTENT_URI, new String[] {ConceptNames.CONCEPT_UUID, ConceptNames.NAME},
ConceptNames.LOCALE + " = ?", new String[] {locale}, null)) {
while (c.moveToNext()) {
sConceptNames.put(c.getString(0), c.getString(1));
}
}
sConceptTypes = new HashMap<>();
try (Cursor c = mContentResolver.query(
Concepts.CONTENT_URI, new String[] {Concepts.UUID, Concepts.CONCEPT_TYPE},
null, null, null)) {
while (c.moveToNext()) {
try {
sConceptTypes.put(c.getString(0), ConceptType.valueOf(c.getString(1)));
} catch (IllegalArgumentException e) { /* bad concept type name */ }
}
}
// Special case: we know this is a date even if it's not in any forms or charts.
sConceptTypes.put(ConceptUuids.ADMISSION_DATE_UUID, ConceptType.DATE);
sLoadedLocale = locale;
}
}
}
/** Gets all the orders for a given patient. */
public List<Order> getOrders(String patientUuid) {
Cursor c = mContentResolver.query(
Orders.CONTENT_URI, null,
Orders.PATIENT_UUID + " = ?", new String[] {patientUuid},
Orders.START_MILLIS);
List<Order> orders = new ArrayList<>();
while (c.moveToNext()) {
orders.add(new Order(
Utils.getString(c, Orders.UUID, ""),
patientUuid,
Utils.getString(c, Orders.INSTRUCTIONS, ""),
Utils.getLong(c, Orders.START_MILLIS, null),
Utils.getLong(c, Orders.STOP_MILLIS, null)));
}
c.close();
return orders;
}
/** Gets all observations for a given patient from the local cache, localized to English. */
// TODO/cleanup: Consider returning a SortedSet<Obs> or a Map<String, SortedSet<ObsPoint>>.
public List<Obs> getObservations(String patientUuid) {
return getObservations(patientUuid, ENGLISH_LOCALE);
}
private Obs obsFromCursor(Cursor c) {
long millis = c.getLong(c.getColumnIndex(Observations.ENCOUNTER_MILLIS));
String conceptUuid = c.getString(c.getColumnIndex(Observations.CONCEPT_UUID));
ConceptType conceptType = sConceptTypes.get(conceptUuid);
String value = c.getString(c.getColumnIndex(Observations.VALUE));
String localizedValue = value;
if (ConceptType.CODED.equals(conceptType)) {
localizedValue = sConceptNames.get(value);
}
return new Obs(millis, conceptUuid, conceptType, value, localizedValue);
}
private @Nullable ObsRow obsrowFromCursor(Cursor c) {
String uuid = c.getString(c.getColumnIndex(Observations.UUID));
long millis = c.getLong(c.getColumnIndex(Observations.ENCOUNTER_MILLIS));
String conceptUuid = c.getString(c.getColumnIndex(Observations.CONCEPT_UUID));
ConceptType conceptType = sConceptTypes.get(conceptUuid);
String value = c.getString(c.getColumnIndex(Observations.VALUE));
String localizedValue = value;
if (ConceptType.CODED.equals(conceptType)) {
localizedValue = sConceptNames.get(value);
}
String conceptName = sConceptNames.get(conceptUuid);
if (conceptName == null){
return null;
}
else {
return new ObsRow(uuid, millis, conceptName, value, localizedValue);
}
}
/** Gets all observations for a given patient, localized for a given locale. */
// TODO/cleanup: Consider returning a SortedSet<Obs> or a Map<String, SortedSet<ObsPoint>>.
public List<Obs> getObservations(String patientUuid, String locale) {
loadConceptData(locale);
List<Obs> results = new ArrayList<>();
try (Cursor c = mContentResolver.query(
Observations.CONTENT_URI, null,
Observations.PATIENT_UUID + " = ? and "
+ Observations.VOIDED + " IS NOT ?",
new String[] {patientUuid,"1"},null)) {
while (c.moveToNext()) {
results.add(obsFromCursor(c));
}
}
return results;
}
public ArrayList<ObsRow> getPatientObservationsByConcept(String patientUuid, String conceptUuid) {
loadConceptData(ENGLISH_LOCALE);
ArrayList<ObsRow> results = new ArrayList<>();
try (
Cursor c = mContentResolver.query(
Observations.CONTENT_URI,
null,
Observations.VOIDED + " IS NOT ? and "
+ Observations.PATIENT_UUID + " = ? and "
+ Observations.CONCEPT_UUID + " = ?",
new String[] {"1",patientUuid,conceptUuid},
Observations.ENCOUNTER_MILLIS + " ASC"
)) {
while (c.moveToNext()) {
ObsRow row = obsrowFromCursor(c);
if (row !=null){results.add(row);}
}
}
return results;
}
public ArrayList<ObsRow> getPatientObservationsByMillis(String patientUuid, String startMillis,String stopMillis) {
loadConceptData(ENGLISH_LOCALE);
ArrayList<ObsRow> results = new ArrayList<>();
String conditions = Observations.VOIDED + " IS NOT ? and "
+ Observations.PATIENT_UUID + " = ? and "
+ Observations.ENCOUNTER_MILLIS + " >= ? and "
+ Observations.ENCOUNTER_MILLIS + " <= ?";
String[] values = new String[]{"1",patientUuid, startMillis,stopMillis};
String order = Observations.ENCOUNTER_MILLIS + " ASC";
try(Cursor c = mContentResolver.query(Observations.CONTENT_URI,null,conditions,values, order))
{
while (c.moveToNext()) {
ObsRow row = obsrowFromCursor(c);
if (row !=null){results.add(row);}
}
}
return results;
}
public ArrayList<ObsRow> getPatientObservationsByConceptMillis(String patientUuid, String conceptUuid, String StartMillis, String StopMillis) {
loadConceptData(ENGLISH_LOCALE);
ArrayList<ObsRow> results = new ArrayList<>();
String conditions = Observations.VOIDED + " IS NOT ? and "
+ Observations.PATIENT_UUID + " = ? and "
+ Observations.CONCEPT_UUID + " = ? and "
+ Observations.ENCOUNTER_MILLIS + " >= ? and "
+ Observations.ENCOUNTER_MILLIS + " <= ?";
String[] values = new String[]{"1",patientUuid, conceptUuid, StartMillis,StopMillis};
String order = Observations.ENCOUNTER_MILLIS + " ASC";
try(Cursor c = mContentResolver.query(Observations.CONTENT_URI,null,conditions,values, order))
{
while (c.moveToNext()) {
ObsRow row = obsrowFromCursor(c);
if (row !=null){results.add(row);}
}
}
return results;
}
/** Gets the latest observation of each concept for a given patient, localized to English. */
// TODO/cleanup: Have this return a Map<String, ObsPoint>.
public Map<String, Obs> getLatestObservations(String patientUuid) {
// TODO: i18n
return getLatestObservations(patientUuid, ENGLISH_LOCALE);
}
/** Gets the latest observation of each concept for a given patient from the app db. */
// TODO/cleanup: Have this return a Map<String, ObsPoint>.
public Map<String, Obs> getLatestObservations(String patientUuid, String locale) {
Map<String, Obs> result = new HashMap<>();
for (Obs obs : getObservations(patientUuid, locale)) {
Obs existing = result.get(obs.conceptUuid);
if (existing == null || obs.time.isAfter(existing.time)) {
result.put(obs.conceptUuid, obs);
}
}
return result;
}
/** Gets the latest observation of the specified concept for all patients. */
// TODO/cleanup: Have this return a Map<String, ObsPoint>.
public Map<String, Obs> getLatestObservationsForConcept(
String conceptUuid, String locale) {
loadConceptData(locale);
try (Cursor c = mContentResolver.query(
Observations.CONTENT_URI, null,
Observations.VOIDED + " IS NOT ? and "
+ Observations.CONCEPT_UUID + " = ?",
new String[] {"1",conceptUuid},
Observations.ENCOUNTER_MILLIS + " DESC")) {
Map<String, Obs> result = new HashMap<>();
while (c.moveToNext()) {
String patientUuid = Utils.getString(c, Observations.PATIENT_UUID);
if (result.containsKey(patientUuid)) continue;
result.put(patientUuid, obsFromCursor(c));
}
return result;
}
}
/** Retrieves and assembles a Chart from the local datastore. */
public List<Chart> getCharts(String uuid) {
Map<Long, ChartSection> tileGroupsById = new HashMap<>();
Map<Long, ChartSection> rowGroupsById = new HashMap<>();
List<Chart> Charts = new ArrayList<>();
Chart currentChart = null;
try (Cursor c = mContentResolver.query(
ChartItems.CONTENT_URI, null,
ChartItems.CHART_UUID + " = ?", new String[] {uuid}, "weight")) {
while (c.moveToNext()) {
Long rowid = Utils.getLong(c, ChartItems.ROWID);
Long parentRowid = Utils.getLong(c, ChartItems.PARENT_ROWID);
String label = Utils.getString(c, ChartItems.LABEL, "");
if (parentRowid == null) {
// Add a section.
String SectionType = Utils.getString(c, ChartItems.SECTION_TYPE);
if (SectionType != null) {
switch (SectionType) {
case "CHART_DIVIDER":
if ((currentChart != null) &&
((currentChart.tileGroups.size() != 0)
|| (currentChart.rowGroups.size() != 0))) {
Charts.add(currentChart);
}
break;
case "TILE_ROW":
ChartSection tileGroup = new ChartSection(label);
currentChart.tileGroups.add(tileGroup);
tileGroupsById.put(rowid, tileGroup);
break;
case "GRID_SECTION":
ChartSection rowGroup = new ChartSection(label);
currentChart.rowGroups.add(rowGroup);
rowGroupsById.put(rowid, rowGroup);
break;
}
}
} else {
// Add a tile to its tile group or a grid row to its row group.
ChartSection section = tileGroupsById.containsKey(parentRowid)
? tileGroupsById.get(parentRowid) : rowGroupsById.get(parentRowid);
if (section != null) {
ChartItem item = new ChartItem(label,
Utils.getString(c, ChartItems.TYPE),
Utils.getLong(c, ChartItems.REQUIRED, 0L) > 0L,
Utils.getString(c, ChartItems.CONCEPT_UUIDS, "").split(","),
Utils.getString(c, ChartItems.FORMAT),
Utils.getString(c, ChartItems.CAPTION_FORMAT),
Utils.getString(c, ChartItems.CSS_CLASS),
Utils.getString(c, ChartItems.CSS_STYLE),
Utils.getString(c, ChartItems.SCRIPT));
section.items.add(item);
} else {
String type = Utils.getString(c, ChartItems.TYPE);
if ((type != null) && (type.equals("CHART_DIVIDER"))) {
currentChart = new Chart(uuid, label);
}
}
}
}
}
Charts.add(currentChart);
return Charts;
}
public List<Form> getForms() {
Cursor cursor = mContentResolver.query(
Contracts.Forms.CONTENT_URI, null, null, null, null);
SortedSet<Form> forms = new TreeSet<>();
try {
while (cursor.moveToNext()) {
forms.add(new Form(
Utils.getString(cursor, Contracts.Forms.UUID),
Utils.getString(cursor, Contracts.Forms.NAME),
Utils.getString(cursor, Contracts.Forms.VERSION)));
}
} finally {
cursor.close();
}
List<Form> sortedForms = new ArrayList<>();
sortedForms.addAll(forms);
return sortedForms;
}
}