/* * #! * % * 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.HashSet; import java.util.Map; import java.util.Set; 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.PositionReport; import de.hub.cs.dbis.lrb.types.TollNotification; import de.hub.cs.dbis.lrb.types.internal.AccidentTuple; import de.hub.cs.dbis.lrb.types.internal.CountTuple; import de.hub.cs.dbis.lrb.types.internal.LavTuple; import de.hub.cs.dbis.lrb.types.util.ISegmentIdentifier; import de.hub.cs.dbis.lrb.types.util.SegmentIdentifier; import de.hub.cs.dbis.lrb.util.Constants; import de.hub.cs.dbis.lrb.util.Time; /** * {@link TollNotificationBolt} calculates the toll for each vehicle and reports it back to the vehicle if a vehicle * enters a segment. Furthermore, the toll is assessed to the vehicle if it leaves a segment.<br /> * <br /> * The toll depends on the number of cars in the segment (the minute before) the car is driving on and is only charged * if the car is not on the exit line, more than 50 cars passed this segment the minute before, the * "latest average velocity" is smaller then 40, and no accident occurred in the minute before in the segment and 4 * downstream segments.<br /> * <br /> * {@link TollNotificationBolt} processes four input streams. The first input is expected to be of type * {@link PositionReport} and must be grouped by vehicle id. The other inputs are expected to be of type * {@link AccidentTuple}, {@link CountTuple}, and {@link LavTuple} and must be broadcasted. All inputs most be ordered * by time (ie, timestamp for {@link PositionReport} and minute number for {@link AccidentTuple}, {@link CountTuple}, * and {@link LavTuple}). It is further assumed, that all {@link AccidentTuple}s and {@link CountTuple}s with a * <em>smaller</em> minute number than a {@link PositionReport} tuple as well as all {@link LavTuple}s with the * <em>same</em> minute number than a {@link PositionReport} tuple are delivered <em>before</em> those * {@link PositionReport}s.<br /> * <br /> * This implementation assumes, that {@link PositionReport}s, {@link AccidentTuple}s, {@link CountTuple}s, and * {@link LavTuple}s are delivered via streams called {@link TopologyControl#POSITION_REPORTS_STREAM_ID}, * {@link TopologyControl#ACCIDENTS_STREAM_ID}, {@link TopologyControl#CAR_COUNTS_STREAM_ID}, and * {@link TopologyControl#LAVS_STREAM_ID}, respectively.<br /> * <br /> * <strong>Expected input:</strong> {@link PositionReport}, {@link AccidentTuple}, {@link CountTuple}, {@link LavTuple}<br /> * <strong>Output schema:</strong> * <ul> * <li>{@link TollNotification} (stream: {@link TopologyControl#TOLL_NOTIFICATIONS_STREAM_ID})</li> * <li>{@link TollNotification} (stream: {@link TopologyControl#TOLL_ASSESSMENTS_STREAM_ID})</li> * </ul> * * @author msoyka * @author richter * @author mjsax */ public class TollNotificationBolt extends BaseRichBolt { private final static long serialVersionUID = 5537727428628598519L; private static final Logger LOGGER = LoggerFactory.getLogger(TollNotificationBolt.class); /** The storm provided output collector. */ private OutputCollector collector; /** Buffer for accidents. */ private Set<ISegmentIdentifier> currentMinuteAccidents = new HashSet<ISegmentIdentifier>(); /** Buffer for accidents. */ private Set<ISegmentIdentifier> previousMinuteAccidents = new HashSet<ISegmentIdentifier>(); /** Buffer for car counts. */ private Map<ISegmentIdentifier, Integer> currentMinuteCounts = new HashMap<ISegmentIdentifier, Integer>(); /** Buffer for car counts. */ private Map<ISegmentIdentifier, Integer> previousMinuteCounts = new HashMap<ISegmentIdentifier, Integer>(); /** Buffer for LAV values. */ private Map<ISegmentIdentifier, Integer> currentMinuteLavs = new HashMap<ISegmentIdentifier, Integer>(); /** Buffer for LAV values. */ private Map<ISegmentIdentifier, Integer> previousMinuteLavs = new HashMap<ISegmentIdentifier, Integer>(); /** * Contains all vehicle IDs and segment of the last {@link PositionReport} to allow skipping already sent * notifications (there's only one notification per segment per vehicle). */ private final Map<Integer, Short> allCars = new HashMap<Integer, Short>(); /** Contains the last toll notification for each vehicle to assess the toll when the vehicle leaves a segment. */ private final Map<Integer, TollNotification> lastTollNotification = new HashMap<Integer, TollNotification>(); /** Internally (re)used object to access individual attributes. */ private final PositionReport inputPositionReport = new PositionReport(); /** Internally (re)used object to access individual attributes. */ private final AccidentTuple inputAccidentTuple = new AccidentTuple(); /** Internally (re)used object to access individual attributes. */ private final CountTuple inputCountTuple = new CountTuple(); /** Internally (re)used object to access individual attributes. */ private final LavTuple inputLavTuple = new LavTuple(); /** Internally (re)used object. */ private final SegmentIdentifier segmentToCheck = new SegmentIdentifier(); /** The currently processed 'minute number'. */ private int currentMinute = -1; @Override public void prepare(@SuppressWarnings("rawtypes") Map stormConf, TopologyContext context, OutputCollector collector) { this.collector = collector; } @Override public void execute(Tuple input) { final String inputStreamId = input.getSourceStreamId(); if(inputStreamId.equals(TimestampMerger.FLUSH_STREAM_ID)) { Object ts = input.getValue(0); if(ts == null) { this.collector.emit(TimestampMerger.FLUSH_STREAM_ID, new Values((Object)null)); } else { this.checkMinute(Time.getMinute(((Number)ts).shortValue())); } this.collector.ack(input); return; } if(inputStreamId.equals(TopologyControl.POSITION_REPORTS_STREAM_ID)) { this.inputPositionReport.clear(); this.inputPositionReport.addAll(input.getValues()); LOGGER.trace(this.inputPositionReport.toString()); this.checkMinute(this.inputPositionReport.getMinuteNumber()); if(this.inputPositionReport.isOnExitLane()) { final TollNotification lastNotification = this.lastTollNotification.remove(this.inputPositionReport .getVid()); if(lastNotification != null) { this.collector.emit(TopologyControl.TOLL_ASSESSMENTS_STREAM_ID, lastNotification); } this.collector.ack(input); return; } final Short currentSegment = this.inputPositionReport.getSegment(); final Integer vid = this.inputPositionReport.getVid(); final Short previousSegment = this.allCars.put(vid, currentSegment); if(previousSegment != null && currentSegment.shortValue() == previousSegment.shortValue()) { this.collector.ack(input); return; } int toll = 0; Integer lav = this.previousMinuteLavs.get(new SegmentIdentifier(this.inputPositionReport)); final int lavValue; if(lav != null) { lavValue = lav.intValue(); } else { lav = new Integer(-1); lavValue = -1; } if(lavValue < 40) { final Integer count = this.previousMinuteCounts.get(new SegmentIdentifier(this.inputPositionReport)); int carCount = 0; if(count != null) { carCount = count.intValue(); } if(carCount > 50) { // downstream is either larger or smaller of current segment final Short direction = this.inputPositionReport.getDirection(); final short dir = direction.shortValue(); // EASTBOUND == 0 => diff := 1 // WESTBOUNT == 1 => diff := -1 final short diff = (short)-(dir - 1 + ((dir + 1) / 2)); assert (dir == Constants.EASTBOUND.shortValue() ? diff == 1 : diff == -1); final Integer xway = this.inputPositionReport.getXWay(); final short curSeg = currentSegment.shortValue(); this.segmentToCheck.setXWay(xway); this.segmentToCheck.setDirection(direction); int i; for(i = 0; i <= 4; ++i) { final short nextSegment = (short)(curSeg + (diff * i)); assert (dir == Constants.EASTBOUND.shortValue() ? nextSegment >= curSeg : nextSegment <= curSeg); this.segmentToCheck.setSegment(new Short(nextSegment)); if(this.previousMinuteAccidents.contains(this.segmentToCheck)) { break; } } if(i == 5) { // only true if no accident was found and "break" was not executed final int var = carCount - 50; toll = 2 * var * var; } } } // TODO get accurate emit time... final TollNotification tollNotification = new TollNotification(this.inputPositionReport.getTime(), this.inputPositionReport.getTime(), vid, lav, new Integer(toll)); final TollNotification lastNotification; if(toll != 0) { lastNotification = this.lastTollNotification.put(vid, new TollNotification(tollNotification.getTime(), tollNotification.getEmit(), tollNotification.getVid(), tollNotification.getSpeed(), tollNotification.getToll())); } else { lastNotification = this.lastTollNotification.remove(vid); } if(lastNotification != null) { this.collector.emit(TopologyControl.TOLL_ASSESSMENTS_STREAM_ID, lastNotification); } this.collector.emit(TopologyControl.TOLL_NOTIFICATIONS_STREAM_ID, tollNotification); } else if(inputStreamId.equals(TopologyControl.ACCIDENTS_STREAM_ID)) { this.inputAccidentTuple.clear(); this.inputAccidentTuple.addAll(input.getValues()); LOGGER.trace(this.inputAccidentTuple.toString()); this.checkMinute(this.inputAccidentTuple.getMinuteNumber()); assert (this.inputAccidentTuple.getMinuteNumber() == this.currentMinute); this.currentMinuteAccidents.add(new SegmentIdentifier(this.inputAccidentTuple)); } else if(inputStreamId.equals(TopologyControl.CAR_COUNTS_STREAM_ID)) { this.inputCountTuple.clear(); this.inputCountTuple.addAll(input.getValues()); LOGGER.trace(this.inputCountTuple.toString()); this.checkMinute(this.inputCountTuple.getMinuteNumber()); assert (this.inputCountTuple.getMinuteNumber() == this.currentMinute); this.currentMinuteCounts.put(new SegmentIdentifier(this.inputCountTuple), this.inputCountTuple.getCount()); } else if(inputStreamId.equals(TopologyControl.LAVS_STREAM_ID)) { this.inputLavTuple.clear(); this.inputLavTuple.addAll(input.getValues()); LOGGER.trace(this.inputLavTuple.toString()); this.checkMinute((short)(this.inputLavTuple.getMinuteNumber() - 1)); assert (this.inputLavTuple.getMinuteNumber() - 1 == this.currentMinute); this.currentMinuteLavs.put(new SegmentIdentifier(this.inputLavTuple), this.inputLavTuple.getLav()); } else { LOGGER.error("Unknown input stream: '" + inputStreamId + "' for tuple " + input); throw new RuntimeException("Unknown input stream: '" + inputStreamId + "' for tuple " + input); } this.collector.ack(input); } private void checkMinute(short minute) { assert (minute >= this.currentMinute); if(minute > this.currentMinute) { LOGGER.trace("New minute: {}", new Short(minute)); this.currentMinute = minute; this.previousMinuteAccidents = this.currentMinuteAccidents; this.currentMinuteAccidents = new HashSet<ISegmentIdentifier>(); this.previousMinuteCounts = this.currentMinuteCounts; this.currentMinuteCounts = new HashMap<ISegmentIdentifier, Integer>(); this.previousMinuteLavs = this.currentMinuteLavs; this.currentMinuteLavs = new HashMap<ISegmentIdentifier, Integer>(); } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declareStream(TopologyControl.TOLL_NOTIFICATIONS_STREAM_ID, TollNotification.getSchema()); declarer.declareStream(TopologyControl.TOLL_ASSESSMENTS_STREAM_ID, TollNotification.getSchema()); declarer.declareStream(TimestampMerger.FLUSH_STREAM_ID, new Fields("ts")); } }