/*
* #!
* %
* Copyright (C) 2014 - 2016 Humboldt-Universität zu Berlin
* %
* 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
* distributed 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 the specific language governing permissions and
* limitations under the License.
* #_
*/
package de.hub.cs.dbis.lrb.operators;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichBolt;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
import de.hub.cs.dbis.aeolus.utils.TimestampMerger;
import de.hub.cs.dbis.lrb.queries.utils.TopologyControl;
import de.hub.cs.dbis.lrb.types.internal.AvgSpeedTuple;
import de.hub.cs.dbis.lrb.types.internal.LavTuple;
import de.hub.cs.dbis.lrb.types.util.SegmentIdentifier;
/**
* {@link LatestAverageVelocityBolt} computes the "latest average velocity" (LAV), ie, the average speed over all
* vehicle within an express way-segment (single direction), over the last five minutes (see Time.getMinute(short)]).
* The input is expected to be of type {@link AvgSpeedTuple}, to be ordered by timestamp, and must be grouped by
* {@link SegmentIdentifier}.<br />
* <br />
* <strong>Input schema:</strong> {@link AvgSpeedTuple}<br />
* <strong>Output schema:</strong> {@link LavTuple} ( (stream: {@link TopologyControl#LAVS_STREAM_ID})
*
* @author msoyka
* @author richter
* @author mjsax
*/
public class LatestAverageVelocityBolt extends BaseRichBolt {
private static final long serialVersionUID = 5537727428628598519L;
private static final Logger LOGGER = LoggerFactory.getLogger(LatestAverageVelocityBolt.class);
/** Storm provided output collector. */
private OutputCollector collector;
/** Internally (re)used object to access individual attributes. */
private final AvgSpeedTuple inputTuple = new AvgSpeedTuple();
/** Internally (re)used object. */
private final SegmentIdentifier segmentIdentifier = new SegmentIdentifier();
/** Holds the (at max) last five average speed value for each segment. */
private final Map<SegmentIdentifier, List<Double>> averageSpeedsPerSegment = new HashMap<SegmentIdentifier, List<Double>>();
/** Holds the (at max) last five minute numbers for each segment. */
private final Map<SegmentIdentifier, List<Short>> minuteNumbersPerSegment = new HashMap<SegmentIdentifier, List<Short>>();
/** The currently processed 'minute number'. */
private short currentMinute = -1;
@Override
public void prepare(@SuppressWarnings("rawtypes") Map stormConf, TopologyContext context, OutputCollector collector) {
this.collector = collector;
}
@Override
public void execute(Tuple input) {
if(input.getSourceStreamId().equals(TimestampMerger.FLUSH_STREAM_ID)) {
Object ts = input.getValue(0);
if(ts == null) {
this.flushBuffer(this.currentMinute + 1);
this.collector.emit(TimestampMerger.FLUSH_STREAM_ID, new Values((Object)null));
} else {
this.checkMinute(((Number)ts).shortValue());
}
this.collector.ack(input);
return;
}
this.inputTuple.clear();
this.inputTuple.addAll(input.getValues());
LOGGER.trace(this.inputTuple.toString());
Short minuteNumber = this.inputTuple.getMinuteNumber();
short m = minuteNumber.shortValue();
this.checkMinute(m);
this.segmentIdentifier.set(this.inputTuple);
List<Double> latestAvgSpeeds = this.averageSpeedsPerSegment.get(this.segmentIdentifier);
List<Short> latestMinuteNumber = this.minuteNumbersPerSegment.get(this.segmentIdentifier);
if(latestAvgSpeeds == null) {
latestAvgSpeeds = new LinkedList<Double>();
this.averageSpeedsPerSegment.put(this.segmentIdentifier.copy(), latestAvgSpeeds);
latestMinuteNumber = new LinkedList<Short>();
this.minuteNumbersPerSegment.put(this.segmentIdentifier.copy(), latestMinuteNumber);
}
latestAvgSpeeds.add(this.inputTuple.getAvgSpeed());
latestMinuteNumber.add(minuteNumber);
// discard all values that are more than 5 minutes older than current minute
while(latestAvgSpeeds.size() > 1) {
if(latestMinuteNumber.get(0).shortValue() < m - 4) {
latestAvgSpeeds.remove(0);
latestMinuteNumber.remove(0);
} else {
break;
}
}
assert (latestAvgSpeeds.size() <= 5);
assert (latestMinuteNumber.size() <= 5);
Integer lav = this.computeLavValue(latestAvgSpeeds);
this.collector.emit(TopologyControl.LAVS_STREAM_ID,
new LavTuple(new Short((short)(((m + 1) * 60) - 1)), this.segmentIdentifier.getXWay(),
this.segmentIdentifier.getSegment(), this.segmentIdentifier.getDirection(), lav));
this.collector.ack(input);
}
private void checkMinute(short minute) {
assert (minute >= this.currentMinute);
if(minute > this.currentMinute) {
LOGGER.trace("new minute: {}", new Short(minute));
// each time we step from one minute to another, we need to check the previous time range for "unfinished"
// open windows; this can happen, if there is not AvgSpeedTuple for a segment in the minute before the
// current one; we need to truncate all open windows and compute LAV values for each open segment
this.flushBuffer(minute);
this.currentMinute = minute;
this.collector.emit(TimestampMerger.FLUSH_STREAM_ID, new Values(new Short((short)((minute * 60) - 1))));
}
}
private void flushBuffer(int m) {
short nextMinute = this.currentMinute;
// a segment can have multiple consecutive missing AvgSpeedTuple
// (for example, if no more cars drive on a segment)
while(nextMinute++ < m) {
Iterator<Entry<SegmentIdentifier, List<Short>>> it = this.minuteNumbersPerSegment.entrySet().iterator();
while(it.hasNext()) {
Entry<SegmentIdentifier, List<Short>> e = it.next();
SegmentIdentifier sid = e.getKey();
List<Short> latestMinuteNumber = e.getValue();
if(latestMinuteNumber.get(latestMinuteNumber.size() - 1).shortValue() < nextMinute - 1) {
List<Double> latestAvgSpeeds = this.averageSpeedsPerSegment.get(sid);
// truncate window if entry is more than 5 minutes older than nextMinute
// (can be at max one entry)
if(latestMinuteNumber.get(0).shortValue() < nextMinute - 5) {
latestAvgSpeeds.remove(0);
latestMinuteNumber.remove(0);
}
if(latestAvgSpeeds.size() > 0) {
Integer lav = this.computeLavValue(latestAvgSpeeds);
this.collector.emit(TopologyControl.LAVS_STREAM_ID, new LavTuple(new Short(
(short)((nextMinute * 60) - 1)), sid.getXWay(), sid.getSegment(), sid.getDirection(), lav));
} else {
// remove empty window completely
it.remove();
this.averageSpeedsPerSegment.remove(sid);
}
}
}
}
}
private Integer computeLavValue(List<Double> latestAvgSpeeds) {
double speedSum = 0;
int valueCount = 0;
for(Double speed : latestAvgSpeeds) {
speedSum += speed.doubleValue();
++valueCount;
}
return new Integer((int)(speedSum / valueCount));
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declareStream(TopologyControl.LAVS_STREAM_ID, LavTuple.getSchema());
declarer.declareStream(TimestampMerger.FLUSH_STREAM_ID, new Fields("ts"));
}
}