/* * #! * % * 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.AccidentNotification; import de.hub.cs.dbis.lrb.types.PositionReport; import de.hub.cs.dbis.lrb.types.internal.AccidentTuple; 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 AccidentNotificationBolt} notifies vehicles approaching an accident (ie, are at most 4 segments upstream of * the accident) to allow them to exit the express way. Vehicles are notified about accidents that occurred in the * minute before the current {@link PositionReport} (ie, 'minute number' [see Time.getMinute(short)])) and as long as * the accident was not cleared and only once per segment (ie, each time a new segment is entered).<br /> * <br /> * {@link AccidentNotificationBolt} processes two input streams. The first input is expected to be of type * {@link PositionReport} and must be grouped by vehicle id. The second input is expected to be of type * {@link AccidentTuple} and must be broadcasted. Both inputs most be ordered by time (ie, timestamp for * {@link PositionReport} and minute number for {@link AccidentTuple}). It is further assumed, that all * {@link AccidentTuple}s with a <em>smaller</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 are delivered via a stream called * {@link TopologyControl#POSITION_REPORTS_STREAM_ID}. Input tuples of all other streams are assumed to be * {@link AccidentTuple}s.<br /> * <br /> * <strong>Input schema:</strong> {@link PositionReport} and {@link AccidentTuple}<br /> * <strong>Output schema:</strong> {@link AccidentNotification} * * @author mjsax * @author trillian */ public class AccidentNotificationBolt extends BaseRichBolt { private static final long serialVersionUID = 5537727428628598519L; private static final Logger LOGGER = LoggerFactory.getLogger(AccidentNotificationBolt.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>(); /** * 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>(); /** 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. */ 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 sourceStreamId = input.getSourceStreamId(); if(sourceStreamId.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(sourceStreamId.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()) { 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; } // upstream 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); for(int 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)) { // TODO get accurate emit time... this.collector.emit(new AccidentNotification(this.inputPositionReport.getTime(), this.inputPositionReport.getTime(), this.segmentToCheck.getSegment(), vid)); break; // send a notification for the closest accident only } } } else { 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)); } 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>(); } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(AccidentNotification.getSchema()); declarer.declareStream(TimestampMerger.FLUSH_STREAM_ID, new Fields("ts")); } }