diff --git a/app/src/main/assets/OpenAPSMA/determine-basal.js b/app/src/main/assets/OpenAPSMA/determine-basal.js index 1492caf3de..4c5a00fade 100644 --- a/app/src/main/assets/OpenAPSMA/determine-basal.js +++ b/app/src/main/assets/OpenAPSMA/determine-basal.js @@ -22,7 +22,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } var bg = glucose_status.glucose; - if (bg < 30) { //Dexcom is in ??? mode or calibrating, do nothing. Asked @benwest for raw data in iter_glucose + if (bg < 38) { //Dexcom is in ??? mode or calibrating, do nothing. Asked @benwest for raw data in iter_glucose rT.error = "CGM is calibrating or in ??? state"; return rT; } diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index b32592b594..4717f6e237 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -159,6 +159,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { QueryBuilder queryBuilder = daoBgReadings.queryBuilder(); queryBuilder.orderBy("timeIndex", false); queryBuilder.limit(1L); + queryBuilder.where().gt("value", 38); PreparedQuery preparedQuery = queryBuilder.prepare(); bgList = daoBgReadings.query(preparedQuery); @@ -196,6 +197,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { queryBuilder.orderBy("timeIndex", true); Where where = queryBuilder.where(); where.ge("timeIndex", mills); + queryBuilder.where().gt("value", 38); PreparedQuery preparedQuery = queryBuilder.prepare(); bgReadings = daoBgreadings.query(preparedQuery); return bgReadings; @@ -281,6 +283,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { List bgReadings; QueryBuilder queryBuilder = daoBgreadings.queryBuilder(); queryBuilder.orderBy("timeIndex", false); + queryBuilder.where().gt("value", 38); queryBuilder.limit(4l); PreparedQuery preparedQuery = queryBuilder.prepare(); bgReadings = daoBgreadings.query(preparedQuery); @@ -300,13 +303,13 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { BgReading last = bgReadings.get(sizeRecords - 3); BgReading last1 = bgReadings.get(sizeRecords - 2); BgReading last2 = bgReadings.get(sizeRecords - 1); - if (last2.value > 30) { + if (last2.value > 38) { minutes = (now.timeIndex - last2.timeIndex)/(60d*1000); change = now.value - last2.value; - } else if (last1.value > 30) { + } else if (last1.value > 38) { minutes = (now.timeIndex - last1.timeIndex)/(60d*1000);; change = now.value - last1.value; - } else if (last.value > 30) { + } else if (last.value > 38) { minutes = (now.timeIndex - last.timeIndex)/(60d*1000); change = now.value - last.value; } else { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/DanaR/DanaRPump.java b/app/src/main/java/info/nightscout/androidaps/plugins/DanaR/DanaRPump.java index 918d533e5e..f088aca1b0 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/DanaR/DanaRPump.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/DanaR/DanaRPump.java @@ -128,21 +128,21 @@ public class DanaRPump { profile.put("dia", dia); JSONArray carbratios = new JSONArray(); - carbratios.put(new JSONObject().put("time", "00:00").put("timeAsSeconds", 0).put("value", nightCF)); - carbratios.put(new JSONObject().put("time", "06:00").put("timeAsSeconds", 6 * 3600).put("value", morningCF)); - carbratios.put(new JSONObject().put("time", "11:00").put("timeAsSeconds", 11 * 3600).put("value", afternoonCF)); - carbratios.put(new JSONObject().put("time", "14:00").put("timeAsSeconds", 17 * 3600).put("value", eveningCF)); - carbratios.put(new JSONObject().put("time", "22:00").put("timeAsSeconds", 22 * 3600).put("value", nightCF)); + carbratios.put(new JSONObject().put("time", "00:00").put("timeAsSeconds", 0).put("value", nightCIR)); + carbratios.put(new JSONObject().put("time", "06:00").put("timeAsSeconds", 6 * 3600).put("value", morningCIR)); + carbratios.put(new JSONObject().put("time", "11:00").put("timeAsSeconds", 11 * 3600).put("value", afternoonCIR)); + carbratios.put(new JSONObject().put("time", "14:00").put("timeAsSeconds", 17 * 3600).put("value", eveningCIR)); + carbratios.put(new JSONObject().put("time", "22:00").put("timeAsSeconds", 22 * 3600).put("value", nightCIR)); profile.put("carbratio", carbratios); profile.put("carbs_hr", car); JSONArray sens = new JSONArray(); - sens.put(new JSONObject().put("time", "00:00").put("timeAsSeconds", 0).put("value", nightCIR)); - sens.put(new JSONObject().put("time", "06:00").put("timeAsSeconds", 6 * 3600).put("value", morningCIR)); - sens.put(new JSONObject().put("time", "11:00").put("timeAsSeconds", 11 * 3600).put("value", afternoonCIR)); - sens.put(new JSONObject().put("time", "17:00").put("timeAsSeconds", 17 * 3600).put("value", eveningCIR)); - sens.put(new JSONObject().put("time", "22:00").put("timeAsSeconds", 22 * 3600).put("value", nightCIR)); + sens.put(new JSONObject().put("time", "00:00").put("timeAsSeconds", 0).put("value", nightCF)); + sens.put(new JSONObject().put("time", "06:00").put("timeAsSeconds", 6 * 3600).put("value", morningCF)); + sens.put(new JSONObject().put("time", "11:00").put("timeAsSeconds", 11 * 3600).put("value", afternoonCF)); + sens.put(new JSONObject().put("time", "17:00").put("timeAsSeconds", 17 * 3600).put("value", eveningCF)); + sens.put(new JSONObject().put("time", "22:00").put("timeAsSeconds", 22 * 3600).put("value", nightCF)); profile.put("sens", sens); JSONArray basals = new JSONArray(); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java index a580861d50..f255e4ca1e 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Dialogs/WizardDialog.java @@ -295,7 +295,7 @@ public class WizardDialog extends DialogFragment implements OnClickListener { bgDiff = lastBgValue - targetBGHigh; } - bg.setText(lastBg.valueToUnitsToString(units) + " ISF: " + DecimalFormatter.to0Decimal(sens)); + bg.setText(lastBg.valueToUnitsToString(units) + " ISF: " + DecimalFormatter.to1Decimal(sens)); bgInsulin.setText(DecimalFormatter.to2Decimal(bgDiff / sens) + "U"); bgInput.removeTextChangedListener(textWatcher); //bgInput.setText(lastBg.valueToUnitsToString(units)); @@ -368,10 +368,10 @@ public class WizardDialog extends DialogFragment implements OnClickListener { BolusWizard wizard = new BolusWizard(); wizard.doCalc(specificProfile, carbsAfterConstraint, c_bg, corrAfterConstraint, bolusIobCheckbox.isChecked(), basalIobCheckbox.isChecked()); - bg.setText(c_bg + " ISF: " + DecimalFormatter.to0Decimal(wizard.sens)); + bg.setText(c_bg + " ISF: " + DecimalFormatter.to1Decimal(wizard.sens)); bgInsulin.setText(DecimalFormatter.to2Decimal(wizard.insulinFromBG) + "U"); - carbs.setText(DecimalFormatter.to0Decimal(c_carbs) + "g IC: " + DecimalFormatter.to0Decimal(wizard.ic)); + carbs.setText(DecimalFormatter.to0Decimal(c_carbs) + "g IC: " + DecimalFormatter.to1Decimal(wizard.ic)); carbsInsulin.setText(DecimalFormatter.to2Decimal(wizard.insulinFromCarbs) + "U"); bolusIobInsulin.setText(DecimalFormatter.to2Decimal(wizard.insulingFromBolusIOB) + "U"); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Notification.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Notification.java new file mode 100644 index 0000000000..abf7858db4 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/Notification.java @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.Overview; + +import java.util.Date; + +/** + * Created by mike on 03.12.2016. + */ + +public class Notification { + public static final int URGENT = 0; + public static final int NORMAL = 1; + public static final int LOW = 2; + public static final int INFO = 3; + + public int id; + Date date; + String text; + Date validTo = new Date(0); + int level; +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/NotificationStore.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/NotificationStore.java new file mode 100644 index 0000000000..10c86ad2a2 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/NotificationStore.java @@ -0,0 +1,60 @@ +package info.nightscout.androidaps.plugins.Overview; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + + +/** + * Created by mike on 03.12.2016. + */ + +public class NotificationStore { + public List store = new ArrayList(); + + public NotificationStore() { + } + + public class NotificationComparator implements Comparator { + @Override + public int compare(Notification o1, Notification o2) { + return o1.level - o2.level; + } + } + + public Notification get(int index) { + return store.get(index); + } + + public void add(Notification n) { + for (int i = 0; i < store.size(); i++) { + if (get(i).id == n.id) { + get(i).date = n.date; + get(i).validTo = n.validTo; + return; + } + } + store.add(n); + Collections.sort(store, new NotificationComparator()); + } + + public void remove(int id) { + for (int i = 0; i < store.size(); i++) { + if (get(i).id == id) { + store.remove(i); + return; + } + } + } + + public void removeExpired() { + for (int i = 0; i < store.size(); i++) { + Notification n = get(i); + if (n.validTo.getTime() != 0 && n.validTo.getTime() < new Date().getTime()) { + store.remove(i); + } + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java index 1d9100f216..7291c80bd8 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewFragment.java @@ -14,6 +14,9 @@ import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v7.app.AlertDialog; +import android.support.v7.widget.CardView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.View; @@ -35,6 +38,7 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.DateFormat; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Calendar; @@ -69,6 +73,8 @@ import info.nightscout.androidaps.plugins.OpenAPSMA.IobTotal; import info.nightscout.androidaps.plugins.Overview.Dialogs.NewTreatmentDialog; import info.nightscout.androidaps.plugins.Overview.Dialogs.WizardDialog; import info.nightscout.androidaps.plugins.Overview.GraphSeriesExtension.PointsWithLabelGraphSeries; +import info.nightscout.androidaps.plugins.Overview.events.EventDismissNotification; +import info.nightscout.androidaps.plugins.Overview.events.EventNewNotification; import info.nightscout.client.data.NSProfile; import info.nightscout.utils.BolusWizard; import info.nightscout.utils.DateUtil; @@ -98,6 +104,9 @@ public class OverviewFragment extends Fragment { TextView apsModeView; GraphView bgGraph; + RecyclerView notificationsView; + LinearLayoutManager llm; + LinearLayout cancelTempLayout; LinearLayout acceptTempLayout; LinearLayout quickWizardLayout; @@ -148,6 +157,11 @@ public class OverviewFragment extends Fragment { quickWizardButton = (Button) view.findViewById(R.id.overview_quickwizard); quickWizardLayout = (LinearLayout) view.findViewById(R.id.overview_quickwizardlayout); + notificationsView = (RecyclerView) view.findViewById(R.id.overview_notifications); + notificationsView.setHasFixedSize(true); + llm = new LinearLayoutManager(view.getContext()); + notificationsView.setLayoutManager(llm); + treatmentButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -383,6 +397,12 @@ public class OverviewFragment extends Fragment { @Subscribe public void onStatusEvent(final EventNewBasalProfile ev) { updateGUIIfVisible(); } + @Subscribe + public void onStatusEvent(final EventNewNotification n) { updateNotifications(); } + + @Subscribe + public void onStatusEvent(final EventDismissNotification n) { updateNotifications(); } + private void hideTempRecommendation() { Activity activity = getActivity(); if (activity != null) @@ -407,6 +427,7 @@ public class OverviewFragment extends Fragment { @SuppressLint("SetTextI18n") public void updateGUI() { + updateNotifications(); BgReading actualBG = MainApp.getDbHelper().actualBg(); BgReading lastBG = MainApp.getDbHelper().lastBg(); if (MainApp.getConfigBuilder() == null || MainApp.getConfigBuilder().getActiveProfile() == null) // app not initialized yet @@ -758,4 +779,95 @@ public class OverviewFragment extends Fragment { } + //Notifications + public static class RecyclerViewAdapter extends RecyclerView.Adapter { + + List notificationsList; + + RecyclerViewAdapter(List notificationsList) { + this.notificationsList = notificationsList; + } + + @Override + public NotificationsViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.overview_notification_item, viewGroup, false); + NotificationsViewHolder notificationsViewHolder = new NotificationsViewHolder(v); + return notificationsViewHolder; + } + + @Override + public void onBindViewHolder(NotificationsViewHolder holder, int position) { + DateFormat df = DateFormat.getTimeInstance(DateFormat.SHORT); + Notification notification = notificationsList.get(position); + holder.dismiss.setTag(notification); + holder.text.setText(notification.text); + holder.time.setText(df.format(notification.date)); + if (notification.level == Notification.URGENT) + holder.cv.setBackgroundColor(MainApp.instance().getResources().getColor(R.color.notificationUrgent)); + else if (notification.level == Notification.NORMAL) + holder.cv.setBackgroundColor(MainApp.instance().getResources().getColor(R.color.notificationNormal)); + else if (notification.level == Notification.LOW) + holder.cv.setBackgroundColor(MainApp.instance().getResources().getColor(R.color.notificationLow)); + else if (notification.level == Notification.INFO) + holder.cv.setBackgroundColor(MainApp.instance().getResources().getColor(R.color.notificationInfo)); + } + + @Override + public int getItemCount() { + return notificationsList.size(); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + } + + public static class NotificationsViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + CardView cv; + TextView time; + TextView text; + Button dismiss; + + NotificationsViewHolder(View itemView) { + super(itemView); + cv = (CardView) itemView.findViewById(R.id.notification_cardview); + time = (TextView) itemView.findViewById(R.id.notification_time); + text = (TextView) itemView.findViewById(R.id.notification_text); + dismiss = (Button) itemView.findViewById(R.id.notification_dismiss); + dismiss.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + Notification notification = (Notification) v.getTag(); + switch (v.getId()) { + case R.id.notification_dismiss: + MainApp.bus().post(new EventDismissNotification(notification.id)); + break; + } + } + } + } + + void updateNotifications() { + Activity activity = getActivity(); + if (activity != null) + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + NotificationStore nstore = getPlugin().notificationStore; + nstore.removeExpired(); + if (nstore.store.size() > 0) { + RecyclerViewAdapter adapter = new RecyclerViewAdapter(nstore.store); + notificationsView.setAdapter(adapter); + notificationsView.setVisibility(View.VISIBLE); + } else { + notificationsView.setVisibility(View.GONE); + } + } + }); + } + + + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewPlugin.java index 86dfa1532f..164238a57a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/OverviewPlugin.java @@ -3,12 +3,16 @@ package info.nightscout.androidaps.plugins.Overview; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import com.squareup.otto.Subscribe; + import org.json.JSONArray; import org.json.JSONException; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.R; import info.nightscout.androidaps.interfaces.PluginBase; +import info.nightscout.androidaps.plugins.Overview.events.EventDismissNotification; +import info.nightscout.androidaps.plugins.Overview.events.EventNewNotification; /** * Created by mike on 05.08.2016. @@ -20,6 +24,8 @@ public class OverviewPlugin implements PluginBase { public QuickWizard quickWizard = new QuickWizard(); + public NotificationStore notificationStore = new NotificationStore(); + public OverviewPlugin() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(MainApp.instance().getApplicationContext()); String storedData = preferences.getString("QuickWizard", "[]"); @@ -28,6 +34,7 @@ public class OverviewPlugin implements PluginBase { } catch (JSONException e) { e.printStackTrace(); } + MainApp.bus().register(this); } @Override @@ -71,4 +78,14 @@ public class OverviewPlugin implements PluginBase { } + @Subscribe + public void onStatusEvent(final EventNewNotification n) { + notificationStore.add(n.notification); + } + + @Subscribe + public void onStatusEvent(final EventDismissNotification n) { + notificationStore.remove(n.id); + } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/events/EventDismissNotification.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/events/EventDismissNotification.java new file mode 100644 index 0000000000..f2414e2a4e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/events/EventDismissNotification.java @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.plugins.Overview.events; + +/** + * Created by mike on 03.12.2016. + */ + +public class EventDismissNotification { + public int id; + + public EventDismissNotification(int did) { + id = did; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Overview/events/EventNewNotification.java b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/events/EventNewNotification.java new file mode 100644 index 0000000000..8a80019f3e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Overview/events/EventNewNotification.java @@ -0,0 +1,15 @@ +package info.nightscout.androidaps.plugins.Overview.events; + +import info.nightscout.androidaps.plugins.Overview.Notification; + +/** + * Created by mike on 03.12.2016. + */ + +public class EventNewNotification { + public Notification notification; + + public EventNewNotification(Notification n) { + notification = n; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Wear/wearintegration/WatchUpdaterService.java b/app/src/main/java/info/nightscout/androidaps/plugins/Wear/wearintegration/WatchUpdaterService.java index b9d42a72f1..fe6f4455c0 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Wear/wearintegration/WatchUpdaterService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Wear/wearintegration/WatchUpdaterService.java @@ -35,6 +35,7 @@ import info.nightscout.androidaps.plugins.Wear.WearPlugin; import info.nightscout.client.data.NSProfile; import info.nightscout.utils.DecimalFormatter; import info.nightscout.utils.SafeParse; +import info.nightscout.utils.ToastUtils; public class WatchUpdaterService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks, @@ -139,7 +140,7 @@ public class WatchUpdaterService extends WearableListenerService implements } } - public void sendData() { + private void sendData() { BgReading lastBG = MainApp.getDbHelper().lastBg(); if (lastBG != null) { @@ -147,14 +148,21 @@ public class WatchUpdaterService extends WearableListenerService implements if(googleApiClient != null && !googleApiClient.isConnected() && !googleApiClient.isConnecting()) { googleApiConnect(); } if (wear_integration) { - new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient).execute(dataMapSingleBG(lastBG, glucoseStatus)); + + final DataMap dataMap = dataMapSingleBG(lastBG, glucoseStatus); + if(dataMap==null) { + ToastUtils.showToastInUiThread(this, getString(R.string.noprofile)); + return; + } + + new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient).execute(dataMap); } } } private DataMap dataMapSingleBG(BgReading lastBG, DatabaseHelper.GlucoseStatus glucoseStatus) { NSProfile profile = MainApp.getConfigBuilder().getActiveProfile().getProfile(); - + if(profile == null) return null; Double lowLine = SafeParse.stringToDouble(mPrefs.getString("low_mark", "0")); Double highLine = SafeParse.stringToDouble(mPrefs.getString("high_mark", "0")); @@ -251,9 +259,16 @@ public class WatchUpdaterService extends WearableListenerService implements if (!graph_bgs.isEmpty()) { DataMap entries = dataMapSingleBG(last_bg, glucoseStatus); + if(entries==null) { + ToastUtils.showToastInUiThread(this, getString(R.string.noprofile)); + return; + } final ArrayList dataMaps = new ArrayList<>(graph_bgs.size()); for (BgReading bg : graph_bgs) { - dataMaps.add(dataMapSingleBG(bg, glucoseStatus)); + DataMap dataMap = dataMapSingleBG(bg, glucoseStatus); + if(dataMap != null) { + dataMaps.add(dataMap); + } } entries.putDataMapArrayList("entries", dataMaps); new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient).execute(entries); @@ -276,7 +291,9 @@ public class WatchUpdaterService extends WearableListenerService implements NSProfile profile = MainApp.getConfigBuilder().getActiveProfile().getProfile(); - if( profile == null) return; + if(profile==null) { + return; + } long beginBasalSegmentTime = startTimeWindow; long runningTime = startTimeWindow; diff --git a/app/src/main/res/layout/overview_fragment.xml b/app/src/main/res/layout/overview_fragment.xml index 34e05a9f60..1f2f066b71 100644 --- a/app/src/main/res/layout/overview_fragment.xml +++ b/app/src/main/res/layout/overview_fragment.xml @@ -13,6 +13,13 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + + + + + + + + + + + + + +