package jamel.util;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilderFactory;
import org.jfree.data.xy.XYSeries;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import jamel.Jamel;
import jamel.data.Export;
import jamel.data.Expression;
import jamel.data.ExpressionFactory;
import jamel.gui.DynamicXYSeries;
import jamel.gui.Gui;
import jamel.gui.Updatable;
/**
* A basic simulation.
*/
public class BasicSimulation implements Simulation {
/**
* Creates and returns a new Gui.
*
* @param elem
* an XML element that contains the description of the Gui.
* @param simulation
* the parent simulation.
* @return a new Gui.
*/
private static Gui getNewGui(final Element elem, final Simulation simulation) {
if (!elem.getNodeName().equals("gui")) {
throw new RuntimeException("Bad element: " + elem.getNodeName());
}
final File guiFile;
final Element guiDescription;
if (elem.hasAttribute("src")) {
/*
* Opens and reads the XML file that contains the specification of the gui.
*/
final String src = elem.getAttribute("src");
final String fileName = simulation.getFile().getParent() + "/" + src;
guiFile = new File(fileName);
final Element root;
try {
final Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(guiFile);
root = document.getDocumentElement();
} catch (final Exception e) {
throw new RuntimeException("Something went wrong while reading \"" + fileName + "\"", e);
}
if (!root.getTagName().equals("gui")) {
throw new RuntimeException(fileName + ": Bad element: " + root.getTagName());
}
guiDescription = root;
} else {
guiFile = simulation.getFile();
guiDescription = elem;
}
final String guiClassName = guiDescription.getAttribute("className");
if (guiClassName.isEmpty()) {
throw new RuntimeException("Attribute \"className\" is missing or empty.");
}
/*
* Creates the gui.
*/
final Gui gui;
try {
gui = (Gui) Class.forName(guiClassName, false, ClassLoader.getSystemClassLoader())
.getConstructor(Element.class, File.class, Simulation.class)
.newInstance(guiDescription, guiFile, simulation);
} catch (Exception e) {
throw new RuntimeException("Something went wrong while creating the gui.", e);
}
return gui;
}
/**
* Creates and returns a new sector.
*
* @param specification
* an XML element that specifies the sector to be created.
* @param simulation
* the parent simulation
*
* @return the new sector.
*/
private static Sector getNewSector(Element specification, Simulation simulation) {
if (!specification.getNodeName().equals("sector")) {
throw new RuntimeException("Bad element: " + specification.getNodeName());
}
final String sectorClassName = specification.getAttribute("className");
if (sectorClassName.isEmpty()) {
throw new RuntimeException("Attribute \"className\" is missing or empty.");
}
final Sector sector;
try {
sector = (Sector) Class.forName(sectorClassName, false, ClassLoader.getSystemClassLoader())
.getConstructor(Element.class, Simulation.class).newInstance(specification, simulation);
} catch (Exception e) {
throw new RuntimeException("Something went wrong while creating the sector.", e);
}
return sector;
}
/** The events. */
private final Map<Integer, Element> events = new HashMap<>();
/**
* The list of the exports.
*/
final private List<Export> exports = new LinkedList<>();
/**
* The expression factory;
*/
final private ExpressionFactory expressionFactory = new ExpressionFactory(this);
/**
* The file that contains the description of the simulation.
*/
final private File file;
/**
* The Gui.
*/
final private Gui gui;
/**
* The name of the simulation.
*/
private final String name;
/**
* A flag that indicates whether the simulation is paused or not.
*/
private boolean pause = true;
/**
* The list of the phases of the period.
*/
final private List<Phase> phases = new LinkedList<>();
/**
* The random.
*/
final private Random random;
/**
* A flag that indicates if the simulation runs.
*/
private boolean run = false;
/**
* An XML element that contains the description of the simulation.
*/
final private Element scenario;
/**
* The collection of the sectors, with access by their names.
*/
final private Map<String, Sector> sectors = new HashMap<>();
/**
* The timer.
*/
final private Timer timer;
/**
* The list of the series to update.
*/
private List<Updatable> updatableSeries = new LinkedList<>();
/**
* Creates an new simulation.
*
* @param scenario
* An XML element that contains the description of the
* simulation.
* @param file
* The file that contains the description of the simulation.
*/
public BasicSimulation(final Element scenario, final File file) {
this.scenario = scenario;
this.file = file;
this.timer = new BasicTimer(0);
this.name = scenario.getAttribute("name");
// Inits the random.
{
final NodeList nodeList = this.scenario.getElementsByTagName("randomSeed");
if (nodeList.getLength() == 0) {
throw new RuntimeException("Missing tag : randomSeed");
}
final int randomSeed = Integer.parseInt(nodeList.item(0).getTextContent().trim());
this.random = new Random(randomSeed);
}
// Looks for the sectors.
{
final Element sectorsTag = (Element) this.scenario.getElementsByTagName("sectors").item(0);
if (sectorsTag == null) {
throw new RuntimeException("Missing tag : sectors");
}
final NodeList nodeList = sectorsTag.getElementsByTagName("sector");
for (int index = 0; index < nodeList.getLength(); index++) {
final Sector sector = getNewSector((Element) nodeList.item(index), this);
this.sectors.put(sector.getName(), sector);
Jamel.println("new sector", sector.getName());
}
}
// Looks for the phases.
{
final Element phasesTag = (Element) this.scenario.getElementsByTagName("phases").item(0);
if (phasesTag == null) {
throw new RuntimeException("Missing tag : phasesTag");
}
final NodeList phaseList = phasesTag.getElementsByTagName("phase");
for (int i = 0; i < phaseList.getLength(); i++) {
final Element phaseTag = (Element) phaseList.item(i);
final String phaseName = phaseTag.getAttribute("name");
final boolean shuffle = phaseTag.getElementsByTagName("shuffle").item(0) != null;
final NodeList sectorList = phaseTag.getElementsByTagName("sector");
for (int j = 0; j < sectorList.getLength(); j++) {
final String sectorName = sectorList.item(j).getTextContent().trim();
final Sector sector = this.sectors.get(sectorName);
if (sector == null) {
throw new RuntimeException("Sector not found: " + sectorName);
}
final Phase phase = sector.getPhase(phaseName, shuffle);
if (phase == null) {
throw new RuntimeException(
"Sector: " + sectorName + ", unable to create the phase: " + phaseName);
}
this.phases.add(phase);
}
}
}
// Looks for the exports.
{
final Element exportsTag = (Element) this.scenario.getElementsByTagName("exports").item(0);
if (exportsTag != null) {
final NodeList exportList = exportsTag.getElementsByTagName("export");
for (int i = 0; i < exportList.getLength(); i++) {
this.exports.add(new Export((Element) exportList.item(i), this));
}
}
}
// Looks for the events.
{
final Element eventsTag = (Element) this.scenario.getElementsByTagName("events").item(0);
if (eventsTag != null) {
final NodeList eventList = eventsTag.getElementsByTagName("event");
for (int i = 0; i < eventList.getLength(); i++) {
final Element element = (Element) eventList.item(i);
final int period = Integer.parseInt(element.getAttribute("when"));
if (this.events.containsKey(period)) {
throw new RuntimeException("Events already defined for the period: " + period);
}
this.events.put(period, element);
}
}
}
// Looks for the gui.
{
final NodeList nodeList = this.scenario.getElementsByTagName("gui");
if (nodeList.getLength() == 0) {
this.gui = null;
} else {
this.gui = getNewGui((Element) nodeList.item(0), this);
}
}
}
/**
* Executes the specified event.
*
* @param event
* the event to be executed.
*/
private void doEvent(Element event) {
switch (event.getTagName()) {
case "pause":
this.pause = true;
break;
default:
throw new RuntimeException("Not yet implemented: \'" + event.getTagName() + "\'");
}
}
/**
* Executes the events of the simulation.
*/
private void doEvents() {
final Element currentEvents = this.events.get(getPeriod());
if (currentEvents != null) {
final NodeList eventList = currentEvents.getChildNodes();
for (int i = 0; i < eventList.getLength(); i++) {
if (eventList.item(i).getNodeType() == Node.ELEMENT_NODE) {
final Element event = (Element) eventList.item(i);
if (event.getAttribute("recipient").isEmpty()) {
this.doEvent(event);
} else if (event.getAttribute("recipient").equals("gui")) {
this.gui.doEvent(event);
} else {
final String sectorName = event.getAttribute("recipient");
this.sectors.get(sectorName).doEvent(event);
}
}
}
}
}
/**
* Pauses the simulation.
*/
private void doPause() {
if (isPaused()) {
// TODO clean up
// final long startPause = new Date().getTime();
this.gui.update();
while (isPaused()) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.gui.update();
// final long endPause = new Date().getTime();
// this.pausedTime += endPause - startPause;
// this.gui.repaintControls(); TODO ??
}
}
/**
* Executes a period of the simulation.
*/
private void period() {
for (final Phase phase : this.phases) {
try {
phase.run();
} catch (Exception e) {
if (this.gui != null) {
this.gui.displayErrorMessage("Error", "Something went wrong.<br>" + "Sector: '"
+ phase.getSector().getName() + "', phase: '" + phase.getName() + "'");
}
throw new RuntimeException("Something went wrong while running the phase: '" + phase.getName()
+ "', for the sector: '" + phase.getSector().getName() + "'.", e);
}
}
for (final Updatable updatable : this.updatableSeries) {
try {
updatable.update();
} catch (Exception e) {
if (this.gui != null) {
this.gui.displayErrorMessage("Error", "Something went wrong while updating the data.");
}
throw new RuntimeException("Something went wrong while updating the data.", e);
}
}
if (gui != null) {
this.gui.update();
}
for (final Export export : this.exports) {
export.run();
}
this.doEvents();
this.doPause();
this.timer.next();
}
@Override
public Expression getDataAccess(final String key) {
final Expression result;
if (key.equals("period")) {
result = new Expression() {
@Override
public Double getValue() {
return (double) timer.getValue();
}
@Override
public String toString() {
return "period";
}
};
} else if (Pattern.matches("value[\\(].*[\\)]", key)) {
final String argString = key.substring(6, key.length() - 1);
final String[] split = argString.split(",");
final Sector sector = this.sectors.get(split[0]);
if (sector == null) {
throw new RuntimeException("Sector not found: " + split[0]);
}
final String[] args = Arrays.copyOfRange(split, 1, split.length);
// TODO le premier argument devrait contenir non seulement le nom du
// secteur, mais aussi (éventuellement) des instructions permettant
// de limiter la sélection à un sous ensemble des agents.
result = sector.getDataAccess(args);
} else {
throw new RuntimeException("Not yet implemented: \'" + key + "\'");
}
return result;
}
@Override
public Expression getExpression(String key) {
return this.expressionFactory.getExpression(key);
}
@Override
public File getFile() {
return this.file;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getPeriod() {
return this.timer.getValue();
}
/**
* Returns the random.
*
* @return the random.
*/
@Override
public Random getRandom() {
return random;
}
@Override
public Sector getSector(final String sectorName) {
return this.sectors.get(sectorName);
}
@Override
public XYSeries getSeries(String x, String y) {
DynamicXYSeries newSeries = null;
try {
final Expression xExp = getExpression(x);
final Expression yExp = getExpression(y);
newSeries = new DynamicXYSeries(xExp, yExp);
this.updatableSeries.add(newSeries);
} catch (final Exception e) {
e.printStackTrace();
}
return newSeries;
}
@Override
public boolean isPaused() {
return this.pause;
}
@Override
public void pause() {
this.pause = !this.pause;
}
@Override
public void run() {
this.run = true;
this.doPause();
while (this.run) {
this.period();
}
}
}