001/**
002 * Copyright 2018 Tampere University of Technology, Pori Department
003 * 
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package service.tut.pori.apilta.shock;
017
018import java.util.ArrayList;
019import java.util.Iterator;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.ListIterator;
023
024import org.apache.commons.lang3.ArrayUtils;
025
026import core.tut.pori.context.ServiceInitializer;
027import service.tut.pori.apilta.ApiltaProperties;
028import service.tut.pori.apilta.shock.datatypes.LocationData;
029import service.tut.pori.apilta.shock.datatypes.ShockMeasurement;
030import service.tut.pori.apilta.utils.MathUtils;
031
032/**
033 * calculate measurement grouping based on the given data
034 * 
035 */
036public class GroupCalculator {
037  private static final String GROUP_METHOD_AVERAGE = "AVERAGE";
038  private static final String GROUP_METHOD_MEDIAN = "MEDIAN";
039  private GroupMethod _method = null;
040  private double _range = 0; // in km
041  private int[] _levelFilter = null;
042  
043  /**
044   * method used for grouping
045   * 
046   */
047  public enum GroupMethod {
048    /**
049     * calculate group value based on average (arithmetic mean)
050     */
051    AVERAGE(GROUP_METHOD_AVERAGE),
052    /**
053     * calculate group value based on median
054     */
055    MEDIAN(GROUP_METHOD_MEDIAN);
056    
057    private String _value;
058    
059    /**
060     * 
061     * @param value
062     */
063    private GroupMethod(String value) {
064      _value = value;
065    }
066    
067    /**
068     * 
069     * @param value
070     * @return group method
071     * @throws IllegalArgumentException
072     */
073    public static GroupMethod fromString(String value) throws IllegalArgumentException {
074      for(GroupMethod m : values()) {
075        if(m._value.equalsIgnoreCase(value)) {
076          return m;
077        }
078      }
079      throw new IllegalArgumentException("Invalid value: "+value);
080    }
081  } // enum GroupMethod
082  
083  /**
084   * 
085   * @param method
086   * @param range in meters, if null or < 1 default is used
087   */
088  public GroupCalculator(GroupMethod method, Integer range) {
089    _method = method;
090    if(range == null || range < 1) {
091      _range = ServiceInitializer.getPropertyHandler().getSystemProperties(ApiltaProperties.class).getShockGroupRange();
092    }else {
093      _range = range;
094      _range /= 1000;
095    }
096  }
097  
098  /**
099   * @param levelFilter the levelFilter to set
100   */
101  public void setLevelFilter(int[] levelFilter) {
102    _levelFilter = levelFilter;
103  }
104
105  /**
106   * 
107   * @param measurements note: the passed list might be modified by this method
108   * @return the grouped list
109   */
110  public List<ShockMeasurement> group(List<ShockMeasurement> measurements) {
111    switch(_method) {
112      case AVERAGE:
113        return groupAverage(measurements);
114      case MEDIAN:
115        return groupMedian(measurements);
116      default:
117        throw new IllegalArgumentException("Unknown method: "+_method);
118    }
119  }
120  
121  /**
122   * 
123   * @param measurements
124   * @return values grouped by median
125   */
126  private List<ShockMeasurement> groupMedian(List<ShockMeasurement> measurements) {
127    ArrayList<ShockMeasurement> grouped = new ArrayList<>();
128    
129    int levelSum = 0;
130    int levelCount = 0;
131    ShockMeasurement center = null;
132    double centerLat = 0;
133    double centerLon = 0;
134    Iterator<ShockMeasurement> iter = measurements.iterator();
135    while(iter.hasNext()) {
136      while(iter.hasNext()) {
137        ShockMeasurement m = iter.next();
138        Integer level = m.getLevel();
139        if(level == null) { // ignore measurements without known level
140          iter.remove();
141          continue;
142        }
143        
144        LocationData d = m.getLocationData();
145        if(center == null) {
146          center = m;
147          centerLat = d.getLatitude();
148          centerLon = d.getLongitude();
149          levelSum += level;
150          ++levelCount;
151          iter.remove();
152        }else if(MathUtils.haversine(centerLat, centerLon, d.getLatitude(), d.getLongitude()) < _range) {
153          levelSum += level;
154          ++levelCount;
155          iter.remove();
156        } // if
157      } // while
158      
159      if(levelCount > 0) {
160        int level = Math.round(levelSum/levelCount);
161        if(_levelFilter == null || ArrayUtils.contains(_levelFilter, level)) {
162          center.setLevel(level);
163          grouped.add(center);
164        }
165      }
166      center = null;
167      levelCount = 0;
168      levelSum = 0;
169      iter = measurements.iterator();
170    } // for
171    
172    return (grouped.isEmpty() ? null : grouped);
173  }
174  
175  /**
176   * 
177   * @param measurements
178   * @return values grouped by median
179   */
180  private List<ShockMeasurement> groupAverage(List<ShockMeasurement> measurements) {
181    ArrayList<ShockMeasurement> grouped = new ArrayList<>();
182    
183    LinkedList<Integer> groupLevels = new LinkedList<>();
184    int levelCount = 0;
185    ShockMeasurement center = null;
186    double centerLat = 0;
187    double centerLon = 0;
188    Iterator<ShockMeasurement> iter = measurements.iterator();
189    while(iter.hasNext()) {
190      while(iter.hasNext()) {
191        ShockMeasurement m = iter.next();
192        Integer level = m.getLevel();
193        if(level == null) { // ignore measurements without known level
194          iter.remove();
195          continue;
196        }
197        
198        LocationData d = m.getLocationData();
199        if(center == null) {
200          center = m;
201          centerLat = d.getLatitude();
202          centerLon = d.getLongitude();
203          groupLevels.add(level);
204          ++levelCount;
205          iter.remove();
206        }else if(MathUtils.haversine(centerLat, centerLon, d.getLatitude(), d.getLongitude()) < _range) {
207          int sLevel = level;
208          ListIterator<Integer> lIter = groupLevels.listIterator(levelCount);
209          while(true) {
210            if(sLevel >= lIter.previous()) {
211              lIter.add(level);
212              break;
213            }else if(!lIter.hasPrevious()) {
214              groupLevels.addFirst(level);
215              break;
216            }
217          }
218          ++levelCount;
219          iter.remove();
220        } // if
221      } // while
222      
223      if(levelCount == 0) {
224        continue;
225      }else if(levelCount % 2 == 0) {
226        int mIndex = levelCount/2;
227        int level = Math.round((groupLevels.get(mIndex)+groupLevels.get(mIndex-1))/2);
228        if(_levelFilter == null || ArrayUtils.contains(_levelFilter, level)) {
229          center.setLevel(level);
230          grouped.add(center);
231        }
232      }else {
233        center.setLevel(groupLevels.get(levelCount/2));
234        grouped.add(center);
235      }
236      center = null;
237      levelCount = 0;
238      groupLevels.clear();
239      iter = measurements.iterator();
240    } // for
241    
242    return (grouped.isEmpty() ? null : grouped);
243  }
244}