package rescuecore2.log; import static rescuecore2.misc.EncodingTools.readBytes; import static rescuecore2.misc.EncodingTools.readInt32; import static rescuecore2.misc.EncodingTools.reallySkip; import java.io.File; import java.io.IOException; import java.io.EOFException; import java.io.RandomAccessFile; import java.io.ByteArrayInputStream; import java.util.Map; import java.util.HashMap; import java.util.Set; import java.util.HashSet; import java.util.NavigableMap; import java.util.TreeMap; import rescuecore2.worldmodel.Entity; import rescuecore2.worldmodel.EntityID; import rescuecore2.worldmodel.WorldModel; import rescuecore2.worldmodel.DefaultWorldModel; import rescuecore2.worldmodel.ChangeSet; import rescuecore2.config.Config; import rescuecore2.registry.Registry; /** A log reader that reads from a file. */ public class FileLogReader extends AbstractLogReader { private static final int KEY_FRAME_BUFFER_MAX_SIZE = 10; private RandomAccessFile file; private int maxTime; private NavigableMap<Integer, WorldModel<? extends Entity>> keyFrames; private Map<Integer, Map<EntityID, Long>> perceptionIndices; private Map<Integer, Long> updatesIndices; private Map<Integer, Long> commandsIndices; private Config config; /** Construct a new FileLogReader. @param name The name of the file to read. @param registry The registry to use for reading log entries. @throws IOException If the file cannot be read. @throws LogException If there is a problem reading the log. */ public FileLogReader(String name, Registry registry) throws IOException, LogException { this(new File(name), registry); } /** Construct a new FileLogReader. @param file The file object to read. @param registry The registry to use for reading log entries. @throws IOException If the file cannot be read. @throws LogException If there is a problem reading the log. */ public FileLogReader(File file, Registry registry) throws IOException, LogException { super(registry); Logger.info("Reading file log: " + file.getAbsolutePath()); this.file = new RandomAccessFile(file, "r"); index(); } @Override public Config getConfig() { return config; } @Override public int getMaxTimestep() throws LogException { return maxTime; } @Override public WorldModel<? extends Entity> getWorldModel(int time) throws LogException { Logger.debug("Getting world model at time " + time); WorldModel<? extends Entity> result = new DefaultWorldModel<Entity>(Entity.class); // Look for a key frame Map.Entry<Integer, WorldModel<? extends Entity>> entry = keyFrames.floorEntry(time); int startTime = entry.getKey(); Logger.trace("Found key frame " + startTime); // Copy the initial conditions Logger.trace("Cloning initial conditions"); for (Entity next : entry.getValue()) { result.addEntity(next.copy()); } // Go through updates and apply them all for (int i = startTime + 1; i <= time; ++i) { ChangeSet updates = getUpdates(i).getChangeSet(); Logger.trace("Merging " + updates.getChangedEntities().size() + " updates for timestep " + i); result.merge(updates); } Logger.trace("Done"); // Remove stale key frames removeStaleKeyFrames(); // Store this as a key frame - it's quite likely that the next timestep will be viewed soon. keyFrames.put(time, result); return result; } @Override public Set<EntityID> getEntitiesWithUpdates(int time) throws LogException { Map<EntityID, Long> timestepMap = perceptionIndices.get(time); if (timestepMap == null) { return new HashSet<EntityID>(); } return timestepMap.keySet(); } @Override public PerceptionRecord getPerception(int time, EntityID entity) throws LogException { Map<EntityID, Long> timestepMap = perceptionIndices.get(time); if (timestepMap == null) { return null; } Long l = timestepMap.get(entity); if (l == null) { return null; } try { file.seek(l); int size = readInt32(file); byte[] bytes = readBytes(size, file); return new PerceptionRecord(new ByteArrayInputStream(bytes)); } catch (IOException e) { throw new LogException(e); } } @Override public CommandsRecord getCommands(int time) throws LogException { Long index = commandsIndices.get(time); if (index == null) { return null; } try { file.seek(index); int size = readInt32(file); byte[] bytes = readBytes(size, file); return new CommandsRecord(new ByteArrayInputStream(bytes)); } catch (IOException e) { throw new LogException(e); } } @Override public UpdatesRecord getUpdates(int time) throws LogException { Long index = updatesIndices.get(time); if (index == null) { return null; } try { file.seek(index); int size = readInt32(file); byte[] bytes = readBytes(size, file); return new UpdatesRecord(new ByteArrayInputStream(bytes)); } catch (IOException e) { throw new LogException(e); } } private void index() throws LogException { try { Registry.setCurrentRegistry(registry); keyFrames = new TreeMap<Integer, WorldModel<? extends Entity>>(); perceptionIndices = new HashMap<Integer, Map<EntityID, Long>>(); updatesIndices = new HashMap<Integer, Long>(); commandsIndices = new HashMap<Integer, Long>(); file.seek(0); int id; RecordType type; boolean startFound = false; do { id = readInt32(file); type = RecordType.fromID(id); if (!startFound) { if (!RecordType.START_OF_LOG.equals(type)) { throw new LogException("Log does not start with correct magic number"); } startFound = true; } indexRecord(type); } while (!RecordType.END_OF_LOG.equals(type)); } catch (EOFException e) { Logger.debug("EOF found"); } catch (IOException e) { throw new LogException(e); } } private void indexRecord(RecordType type) throws IOException, LogException { switch (type) { case START_OF_LOG: indexStart(); break; case INITIAL_CONDITIONS: indexInitialConditions(); break; case PERCEPTION: indexPerception(); break; case COMMANDS: indexCommands(); break; case UPDATES: indexUpdates(); break; case CONFIG: indexConfig(); break; case END_OF_LOG: indexEnd(); break; default: throw new LogException("Unexpected record type: " + type); } } private void indexStart() throws IOException { int size = readInt32(file); reallySkip(file, size); } private void indexEnd() throws IOException { int size = readInt32(file); reallySkip(file, size); } private void indexInitialConditions() throws IOException, LogException { int size = readInt32(file); if (size < 0) { throw new LogException("Invalid initial conditions size: " + size); } byte[] bytes = readBytes(size, file); InitialConditionsRecord record = new InitialConditionsRecord(new ByteArrayInputStream(bytes)); keyFrames.put(0, record.getWorldModel()); } private void indexPerception() throws IOException, LogException { long position = file.getFilePointer(); int size = readInt32(file); byte[] bytes = readBytes(size, file); PerceptionRecord record = new PerceptionRecord(new ByteArrayInputStream(bytes)); int time = record.getTime(); EntityID agentID = record.getEntityID(); Map<EntityID, Long> timestepMap = perceptionIndices.get(time); if (timestepMap == null) { timestepMap = new HashMap<EntityID, Long>(); perceptionIndices.put(time, timestepMap); } timestepMap.put(agentID, position); } private void indexCommands() throws IOException, LogException { long position = file.getFilePointer(); int size = readInt32(file); byte[] bytes = readBytes(size, file); CommandsRecord record = new CommandsRecord(new ByteArrayInputStream(bytes)); int time = record.getTime(); commandsIndices.put(time, position); maxTime = Math.max(time, maxTime); } private void indexUpdates() throws IOException, LogException { long position = file.getFilePointer(); int size = readInt32(file); byte[] bytes = readBytes(size, file); UpdatesRecord record = new UpdatesRecord(new ByteArrayInputStream(bytes)); int time = record.getTime(); updatesIndices.put(time, position); maxTime = Math.max(time, maxTime); } private void indexConfig() throws IOException, LogException { int size = readInt32(file); byte[] bytes = readBytes(size, file); ConfigRecord record = new ConfigRecord(new ByteArrayInputStream(bytes)); config = record.getConfig(); } private void removeStaleKeyFrames() { Logger.trace("Removing stale key frames"); int size = keyFrames.size(); if (size < KEY_FRAME_BUFFER_MAX_SIZE) { Logger.trace("Key frame buffer is not full: " + size + (size == 1 ? " entry" : " entries")); return; } // Try to balance the number of key frames. int window = maxTime / KEY_FRAME_BUFFER_MAX_SIZE; for (int i = 0; i < maxTime; i += window) { NavigableMap<Integer, WorldModel<? extends Entity>> next = keyFrames.subMap(i, false, i + window, true); Logger.trace("Window " + i + " -> " + (i + window) + " has " + next.size() + " entries"); if (next.size() > 1) { // Remove all but the last entry in this window Map.Entry<Integer, WorldModel<? extends Entity>> last = next.lastEntry(); next.clear(); next.put(last.getKey(), last.getValue()); Logger.trace("Retained entry " + last); } } Logger.trace("New key frame set: " + keyFrames); } }