/* Copyright (C) 2020-2024 José Rebelo, Petr Vaněk, Reiner Herrmann, Sebastian Krey This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.*; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; public class ActivitySummaryJsonSummary { private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryJsonSummary.class); private JSONObject groupData; private JSONObject summaryData; private JSONObject summaryGroupedList; private ActivitySummaryParser summaryParser; private BaseActivitySummary baseActivitySummary; public ActivitySummaryJsonSummary(final ActivitySummaryParser summaryParser, BaseActivitySummary baseActivitySummary){ this.summaryParser=summaryParser; this.baseActivitySummary=baseActivitySummary; } private JSONObject setSummaryData(BaseActivitySummary item){ String summary = getCorrectSummary(item); JSONObject jsonSummary = getJSONSummary(summary); if (jsonSummary != null) { //add additionally computed values here if (item.getBaseAltitude() != null && item.getBaseAltitude() != -20000) { JSONObject baseAltitudeValues; try { baseAltitudeValues = new JSONObject(); baseAltitudeValues.put("value", item.getBaseAltitude()); baseAltitudeValues.put("unit", "meters"); jsonSummary.put("baseAltitude", baseAltitudeValues); } catch (JSONException e) { e.printStackTrace(); } } if (jsonSummary.has("distanceMeters") && jsonSummary.has("activeSeconds")) { JSONObject averageSpeed; try { JSONObject distanceMeters = (JSONObject) jsonSummary.get("distanceMeters"); JSONObject activeSeconds = (JSONObject) jsonSummary.get("activeSeconds"); double distance = distanceMeters.getDouble("value"); double duration = activeSeconds.getDouble("value"); averageSpeed = new JSONObject(); averageSpeed.put("value", distance / duration); averageSpeed.put("unit", "meters_second"); jsonSummary.put("averageSpeed", averageSpeed); } catch (JSONException e) { e.printStackTrace(); } } } return jsonSummary; } public JSONObject getSummaryData(){ //returns json with summaryData if (summaryData==null) summaryData=setSummaryData(baseActivitySummary); return summaryData; } private String getCorrectSummary(BaseActivitySummary item){ if (item.getRawSummaryData() != null) { try { item = summaryParser.parseBinaryData(item); } catch (final Exception e) { LOG.error("Failed to re-parse corrected summary", e); } } return item.getSummaryData(); } private JSONObject getJSONSummary(String sumData){ JSONObject summarySubdata = null; if (sumData != null) { try { summarySubdata = new JSONObject(sumData); } catch (JSONException e) { } } return summarySubdata; } public JSONObject getSummaryGroupedList() { //returns list grouped by activity groups as per createActivitySummaryGroups if (summaryData==null) summaryData=setSummaryData(baseActivitySummary); if (summaryGroupedList==null) summaryGroupedList=setSummaryGroupedList(summaryData); return summaryGroupedList; } private JSONObject setSummaryGroupedList(JSONObject summaryDatalist){ this.groupData = createActivitySummaryGroups(); //structure for grouping activities into groups, when vizualizing if (summaryDatalist == null) return null; Iterator keys = summaryDatalist.keys(); final Map activeGroups = new LinkedHashMap<>(); // Initialize activeGroups with the initial expected order and empty arrays final Iterator names = this.groupData.keys(); while (names.hasNext()) { activeGroups.put(names.next(), new JSONArray()); } while (keys.hasNext()) { String key = keys.next(); try { JSONObject innerData = (JSONObject) summaryDatalist.get(key); Object value = innerData.get("value"); String unit = innerData.getString("unit"); // Use the group if specified in the entry, otherwise fallback to the array below String groupName = innerData.optString("group", getGroup(key)); JSONArray group = activeGroups.get(groupName); if (group == null) { // This group is not defined in createActivitySummaryGroups - add it to the end group = new JSONArray(); activeGroups.put(groupName, group); } JSONObject item = new JSONObject(); item.put("name", key); item.put("value", value); item.put("unit", unit); group.put(item); } catch (JSONException e) { LOG.error("SportsActivity internal error building grouped summary", e); } } // Convert activeGroups to the expected JSONObject // activeGroups is already ordered final JSONObject grouped = new JSONObject(); for (final Map.Entry entry : activeGroups.entrySet()) { if (entry.getValue().length() == 0) { // empty group continue; } try { grouped.put(entry.getKey(), entry.getValue()); } catch (JSONException e) { LOG.error("SportsActivity internal error building grouped summary", e); } } return grouped; } private String getGroup(String searchItem) { // NB: Default group must be present in group JSONObject created by createActivitySummaryGroups String defaultGroup = "Activity"; if (groupData == null) return defaultGroup; Iterator keys = groupData.keys(); while (keys.hasNext()) { String key = keys.next(); try { JSONArray itemList = (JSONArray) groupData.get(key); for (int i = 0; i < itemList.length(); i++) { if (itemList.getString(i).equals(searchItem)) { return key; } } } catch (JSONException e) { LOG.error("SportsActivity", e); } } return defaultGroup; } private JSONObject createActivitySummaryGroups(){ final Map> groupDefinitions = new LinkedHashMap>() {{ // NB: Default group Activity must be present in this definition, otherwise it wouldn't // be shown. put("Activity", Arrays.asList( DISTANCE_METERS, STEPS, STEP_RATE_SUM, ACTIVE_SECONDS, CALORIES_BURNT, STRIDE_TOTAL, HR_AVG, HR_MAX, HR_MIN, STRIDE_AVG, STRIDE_MAX, STRIDE_MIN, STEP_LENGTH_AVG )); put("Speed", Arrays.asList( SPEED_AVG, SPEED_MAX, SPEED_MIN, PACE_AVG_SECONDS_KM, PACE_MIN, PACE_MAX, "averageSpeed2", CADENCE_AVG, CADENCE_MAX, CADENCE_MIN, STEP_RATE_AVG )); put("Elevation", Arrays.asList( ASCENT_METERS, DESCENT_METERS, ALTITUDE_MAX, ALTITUDE_MIN, ALTITUDE_AVG, ALTITUDE_BASE, ASCENT_SECONDS, DESCENT_SECONDS, FLAT_SECONDS, ASCENT_DISTANCE, DESCENT_DISTANCE, FLAT_DISTANCE, ELEVATION_GAIN, ELEVATION_LOSS )); put("HeartRateZones", Arrays.asList( HR_ZONE_NA, HR_ZONE_WARM_UP, HR_ZONE_FAT_BURN, HR_ZONE_AEROBIC, HR_ZONE_ANAEROBIC, HR_ZONE_EXTREME )); put("Strokes", Arrays.asList( STROKE_DISTANCE_AVG, STROKE_AVG_PER_SECOND, STROKES, STROKE_RATE_AVG, STROKE_RATE_MAX )); put("Swimming", Arrays.asList( SWOLF_INDEX, SWOLF_AVG, SWOLF_MAX, SWOLF_MIN, SWIM_STYLE )); put("TrainingEffect", Arrays.asList( TRAINING_EFFECT_AEROBIC, TRAINING_EFFECT_ANAEROBIC, WORKOUT_LOAD, MAXIMUM_OXYGEN_UPTAKE )); put("laps", Arrays.asList( LAP_PACE_AVERAGE, LAPS, LANE_LENGTH )); put("Pace", Arrays.asList( )); put("RunningForm", Arrays.asList( GROUND_CONTACT_TIME_AVG, IMPACT_AVG, IMPACT_MAX, SWING_ANGLE_AVG, FORE_FOOT_LANDINGS, MID_FOOT_LANDINGS, BACK_FOOT_LANDINGS, EVERSION_ANGLE_AVG, EVERSION_ANGLE_MAX )); }}; return new JSONObject(groupDefinitions); } }