001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins.streetside.history; 003 004import java.util.ArrayList; 005import java.util.List; 006import java.util.Objects; 007 008import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage; 009 010import org.openstreetmap.josm.plugins.streetside.history.commands.StreetsideCommand; 011import org.openstreetmap.josm.plugins.streetside.history.commands.StreetsideExecutableCommand; 012 013/** 014* History record system in order to let the user undo and redo commands. 015* 016* @author nokutu 017* 018*/ 019public class StreetsideRecord { 020/** The unique instance of the class. */ 021private static StreetsideRecord instance; 022 023private final List<StreetsideRecordListener> listeners = new ArrayList<>(); 024 025/** The set of commands that have taken place or that have been undone. */ 026public List<StreetsideCommand> commandList; 027/** Last written command. */ 028public int position; 029 030/** 031* Main constructor. 032*/ 033public StreetsideRecord() { 034 this.commandList = new ArrayList<>(); 035 this.position = -1; 036} 037 038/** 039* Returns the unique instance of the class. 040* 041* @return The unique instance of the class. 042*/ 043public static synchronized StreetsideRecord getInstance() { 044 if (StreetsideRecord.instance == null) 045 StreetsideRecord.instance = new StreetsideRecord(); 046 return StreetsideRecord.instance; 047} 048 049/** 050* Adds a listener. 051* 052* @param lis 053* The listener to be added. 054*/ 055public void addListener(StreetsideRecordListener lis) { 056 this.listeners.add(lis); 057} 058 059/** 060* Removes the given listener. 061* 062* @param lis 063* The listener to be removed. 064*/ 065public void removeListener(StreetsideRecordListener lis) { 066 this.listeners.remove(lis); 067} 068 069/** 070* Adds a new command to the list. 071* 072* @param command 073* The command to be added. 074*/ 075public void addCommand(final StreetsideCommand command) { 076 077 if (command instanceof StreetsideExecutableCommand) 078 ((StreetsideExecutableCommand) command).execute(); 079 // Checks if it is a continuation of last command 080 if (this.position != -1) { 081 boolean equalSets = true; 082 for (StreetsideAbstractImage img : this.commandList.get(this.position).images) { 083 equalSets = command.images.contains(img) && equalSets; 084 } 085 for (StreetsideAbstractImage img : command.images) { 086 equalSets = this.commandList.get(this.position).images.contains(img) && equalSets; 087 } 088 if (equalSets && this.commandList.get(this.position).getClass() == command.getClass()) { 089 this.commandList.get(this.position).sum(command); 090 fireRecordChanged(); 091 return; 092 } 093 } 094 // Adds the command to the last position of the list. 095 this.commandList.add(this.position + 1, command); 096 this.position++; 097 while (this.commandList.size() > this.position + 1) { 098 this.commandList.remove(this.position + 1); 099 } 100 fireRecordChanged(); 101} 102 103/** 104* Undo latest command. 105*/ 106public void undo() { 107 if (this.position <= -1) { 108 return; 109 } 110 this.commandList.get(this.position).undo(); 111 this.position--; 112 fireRecordChanged(); 113} 114 115/** 116* Redoes latest undone action. 117*/ 118public void redo() { 119 if (position + 1 >= commandList.size()) { 120 return; 121 } 122 this.position++; 123 this.commandList.get(this.position).redo(); 124 fireRecordChanged(); 125} 126 127private void fireRecordChanged() { 128 this.listeners.stream().filter(Objects::nonNull).forEach(StreetsideRecordListener::recordChanged); 129} 130 131/** 132* Resets the object to its start state. 133*/ 134public void reset() { 135 this.commandList.clear(); 136 this.position = -1; 137} 138}