Fully working app

This commit is contained in:
FyloZ 2022-06-15 15:27:06 -04:00
parent 6f1a3b91aa
commit d4af46450c
Signed by: william
GPG Key ID: 835378AE9AF4AE97
18 changed files with 438 additions and 130 deletions

View File

@ -1,7 +1,9 @@
package configuration;
import metadata.BuildingMetadata;
import simulation.Building;
import simulation.Route;
import simulation.Warehouse;
import java.util.Collection;
@ -21,4 +23,10 @@ public class SimulationData {
public Collection<Route> getRoutes() {
return routes;
}
public Warehouse getWarehouse() {
return (Warehouse) buildings.stream()
.filter(b -> b.getType().equals(BuildingMetadata.TYPE_WAREHOUSE))
.findFirst().get();
}
}

View File

@ -162,9 +162,24 @@ public class XmlConfigurationParser implements ConfigurationParser {
}
}
subscribeFactories(buildings);
return new SimulationData(buildings, routes);
}
private void subscribeFactories(Collection<Building> buildings) {
Warehouse warehouse = (Warehouse) buildings.stream()
.filter(b -> b.getType().equals(BuildingMetadata.TYPE_WAREHOUSE))
.findFirst().get();
for (Building building : buildings) {
if (!(building instanceof Factory)) {
continue;
}
warehouse.attach(((Factory) building));
}
}
private Building parseBuilding(Element buildingElement, Map<String, BuildingMetadata> buildingMetadata) {
String type = buildingElement.getAttribute(ATTRIBUTE_TYPE);
int id = Integer.parseInt(buildingElement.getAttribute(ATTRIBUTE_ID));

View File

@ -60,7 +60,7 @@
<usine type="usine-matiere" id="11" x="32" y="32"/>
<usine type="usine-aile" id="21" x="320" y="32"/>
<usine type="usine-assemblage" id="41" x="160" y="192"/>
<usine type="entrepot" id="51" x="440" y="192"/> <!-- x=640 -->
<usine type="entrepot" id="51" x="640" y="192"/>
<usine type="usine-matiere" id="13" x="544" y="576"/>
<usine type="usine-matiere" id="12" x="96" y="352"/>
<usine type="usine-moteur" id="31" x="320" y="352"/>

View File

@ -1,5 +1,7 @@
package simulation;
import metadata.BuildingMetadata;
import java.util.Optional;
public abstract class Building {
@ -18,6 +20,7 @@ public abstract class Building {
public abstract void processInput(Component input);
public abstract Optional<Component> update();
public abstract BuildingMetadata getMetadata();
public int getId() {
return id;

View File

@ -15,6 +15,10 @@ public enum ComponentType {
this.typeName = typeName;
}
public String getTypeName() {
return typeName;
}
public static ComponentType getForTypeName(String typeName) {
return Arrays.stream(ComponentType.values())
.filter(t -> Objects.equals(t.typeName, typeName))

View File

@ -1,5 +1,6 @@
package simulation;
import metadata.BuildingMetadata;
import metadata.FactoryInput;
import metadata.FactoryMetadata;
@ -7,11 +8,12 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class Factory extends Building {
public class Factory extends Building implements WarehouseObserver {
private final FactoryMetadata metadata;
private final Map<ComponentType, Integer> inputsCount = new HashMap<>();
private boolean isProductionStarted;
private boolean warehouseFull;
private long productionTicks = 0L;
public Factory(int id, String type, int x, int y, FactoryMetadata metadata) {
@ -27,20 +29,11 @@ public class Factory extends Building {
@Override
public Optional<Component> update() {
if (!isProductionStarted) return Optional.empty();
if (!isProductionStarted || (productionTicks == 0 && warehouseFull)) return Optional.empty();
productionTicks++;
float productionInterval = metadata.getProductionInterval();
if (productionTicks < productionInterval) {
if (productionTicks / productionInterval >= (2 / 3f)) {
state = BuildingState.FULL;
} else if (productionTicks / productionInterval >= (1 / 3f)) {
state = BuildingState.TWO_THIRD;
} else {
state = BuildingState.ONE_THIRD;
}
if (productionTicks < metadata.getProductionInterval()) {
updateState();
return Optional.empty();
}
@ -68,6 +61,27 @@ public class Factory extends Building {
}
}
@Override
public BuildingMetadata getMetadata() {
return metadata;
}
@Override
public void updateWarehouseState(boolean full) {
warehouseFull = full;
}
private void updateState() {
float productionRatio = productionTicks / (float) metadata.getProductionInterval();
if (productionRatio >= (2 / 3f)) {
state = BuildingState.FULL;
} else if (productionRatio >= (1 / 3f)) {
state = BuildingState.TWO_THIRD;
} else {
state = BuildingState.ONE_THIRD;
}
}
private boolean hasEnoughComponents() {
for (FactoryInput input : metadata.getInputs()) {
if (!inputsCount.containsKey(input.getType()) || inputsCount.get(input.getType()) < input.getQuantity()) {
@ -94,16 +108,12 @@ public class Factory extends Building {
if (!inputsCount.isEmpty()) {
for (ComponentType type : inputsCount.keySet()) {
builder.append(type.toString())
.append(": ")
.append(inputsCount.get(type));
builder.append(type.toString()).append(": ").append(inputsCount.get(type));
}
}
if (isProductionStarted) {
builder.append(" (interval: ")
.append(productionTicks)
.append(')');
builder.append(" (interval: ").append(productionTicks).append(')');
}
builder.append(']');

View File

@ -0,0 +1,15 @@
package simulation;
import java.util.Random;
public class RandomSellStrategy implements SellStrategy {
private static final int SELL_CHANCE_PERCENT = 1;
private final Random random = new Random();
@Override
public boolean shouldSell() {
int gen = random.nextInt(100);
return gen < SELL_CHANCE_PERCENT;
}
}

View File

@ -0,0 +1,5 @@
package simulation;
public interface SellStrategy {
boolean shouldSell();
}

View File

@ -0,0 +1,12 @@
package simulation;
public class TimedSellStrategy implements SellStrategy {
private static final int SELL_INTERVAL = 300;
private long tick = 0L;
@Override
public boolean shouldSell() {
return ++tick % SELL_INTERVAL == 0;
}
}

View File

@ -1,7 +1,5 @@
package simulation;
import configuration.SimulationConfiguration;
import configuration.SimulationConfigurationSingleton;
import metadata.BuildingMetadata;
import metadata.WarehouseMetadata;
@ -11,26 +9,62 @@ import java.util.Optional;
public class Warehouse extends Building implements WarehouseSubject {
private final WarehouseMetadata metadata;
private final int capacity;
private final Collection<WarehouseObserver> observers = new ArrayList<>();
private SellStrategy sellStrategy = new TimedSellStrategy();
private int planeCount = 0;
public Warehouse(int id, String type, int x, int y, WarehouseMetadata metadata) {
super(id, type, x, y);
this.metadata = metadata;
this.capacity = metadata.getInput().getCapacity();
}
@Override
public Optional<Component> update() {
if (planeCount > 0 && sellStrategy.shouldSell()) {
sellPlane();
}
return Optional.empty();
}
@Override
public void processInput(Component input) {
planeCount++;
updateState();
notifyObservers();
}
private void sellComponent() {
@Override
public BuildingMetadata getMetadata() {
return metadata;
}
public void setSellStrategy(SellStrategy sellStrategy) {
this.sellStrategy = sellStrategy;
}
private void updateState() {
float fullRatio = planeCount / (float) capacity;
if (fullRatio >= 1) {
state = BuildingState.FULL;
} else if (fullRatio >= (2 / 3f)) {
state = BuildingState.TWO_THIRD;
} else if (fullRatio >= (1 / 3f)) {
state = BuildingState.ONE_THIRD;
} else {
state = BuildingState.EMPTY;
}
}
private void sellPlane() {
// Vend un avion
planeCount--;
updateState();
notifyObservers();
}
public void attach(WarehouseObserver observer) {
@ -42,20 +76,13 @@ public class Warehouse extends Building implements WarehouseSubject {
}
public void notifyObservers() {
boolean isFull = planeCount >= getCapacity();
observers.forEach(o -> o.update(isFull));
}
private static int getCapacity() {
SimulationConfiguration configuration = SimulationConfigurationSingleton.getInstance().getConfiguration();
WarehouseMetadata metadata = (WarehouseMetadata) configuration.getMetadata().get(BuildingMetadata.TYPE_WAREHOUSE);
return metadata.getInput().getCapacity();
boolean isFull = planeCount >= capacity;
observers.forEach(o -> o.updateWarehouseState(isFull));
}
@Override
public String toString() {
boolean isFull = planeCount >= getCapacity();
boolean isFull = planeCount >= capacity;
return String.format("[planes: %d, full: %s]", planeCount, isFull);
}

View File

@ -6,5 +6,5 @@ public interface WarehouseObserver {
*
* @param full Si l'entrepot est plein ou non
*/
void update(boolean full);
void updateWarehouseState(boolean full);
}

View File

@ -3,59 +3,67 @@ package view;
import configuration.SimulationConfiguration;
import configuration.SimulationConfigurationSingleton;
import configuration.SimulationData;
import metadata.BuildingMetadata;
import simulation.Building;
import simulation.Component;
import simulation.*;
import simulation.Route;
import view.components.BuildingIcon;
import view.components.ComponentIcon;
import view.components.RouteLine;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.Serial;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class MainPanel extends JPanel {
public class MainPanel extends JLayeredPane {
@Serial
private static final long serialVersionUID = 1L;
private final Map<String, Map<BuildingState, Image>> buildingIcons = new HashMap<>();
private final Map<ComponentType, Image> componentsIcons = new HashMap<>();
private final Map<Integer, Building> buildingsById = new HashMap<>();
private final Collection<ComponentRoute> componentRoutes = new ArrayList<>();
private final Collection<ComponentIcon> componentIcons = new ArrayList<>();
private SimulationConfiguration configuration;
private SimulationData simulationData;
public MainPanel() {
try {
componentsIcons.put(ComponentType.METAL, loadImage("src/ressources/metal.png"));
componentsIcons.put(ComponentType.MOTOR, loadImage("src/ressources/moteur.png"));
componentsIcons.put(ComponentType.PLANE, loadImage("src/ressources/avion.png"));
componentsIcons.put(ComponentType.WING, loadImage("src/ressources/aile.png"));
} catch (IOException e) {
throw new RuntimeException("Could not load components icons", e);
}
}
public void reloadSimulation() {
configuration = SimulationConfigurationSingleton.getInstance().getConfiguration();
simulationData = configuration.getSimulationData();
buildingsById.clear();
simulationData.getBuildings().forEach(b -> buildingsById.put(b.getId(), b));
buildingIcons.clear();
configuration.getMetadata().values().forEach(m -> {
try {
loadBuildingImages(m);
} catch (IOException e) {
System.err.println("Could not load simulation icons");
}
});
int routesOffset = 0;
for (Building building : simulationData.getBuildings()) {
BuildingIcon icon = this.initializeBuilding(building);
routesOffset = icon.getWidth() / 2;
}
int x = 0;
for (Route route : simulationData.getRoutes()) {
initializeRoute(route, routesOffset);
}
}
private BuildingIcon initializeBuilding(Building building) {
buildingsById.put(building.getId(), building);
BuildingIcon icon = new BuildingIcon(building);
add(icon);
setLayer(icon, 2);
return icon;
}
private void initializeRoute(Route route, int offset) {
Building from = getBuildingById(route.getFrom());
Building to = getBuildingById(route.getTo());
RouteLine line = new RouteLine(from, to, offset);
add(line);
setLayer(line, 0);
}
@Override
public void paint(Graphics g) {
@ -63,30 +71,29 @@ public class MainPanel extends JPanel {
if (configuration == null) return;
simulationData.getRoutes().forEach(r -> paintRoute(r, g));
simulationData.getBuildings().forEach(b -> paintBuilding(b, g));
updateBuildings();
Collection<ComponentRoute> removedComponentRoutes = new ArrayList<>();
for (ComponentRoute route : componentRoutes) {
paintComponentRoute(route, g);
Collection<ComponentIcon> removedIcons = new ArrayList<>();
if (route.isTransitFinished()) {
removedComponentRoutes.add(route);
route.sendToOutputBuilding();
for (ComponentIcon icon : componentIcons) {
if (icon.isAtDestination()) {
removedIcons.add(icon);
}
}
// Prevent concurrency problems
componentRoutes.removeAll(removedComponentRoutes);
removedIcons.forEach(i -> {
componentIcons.remove(i);
remove(i);
Building outputBuilding = getBuildingById(i.getDestinationId());
outputBuilding.processInput(i.getComponent());
});
}
private void paintBuilding(Building building, Graphics g) {
building.update().ifPresent(component -> addComponentRoute(component, building));
Image icon = buildingIcons.get(building.getType()).get(building.getState());
g.drawImage(icon, building.getX(), building.getY(), null);
g.drawString(building.toString(), building.getX() - 20, building.getY() - 5);
private void updateBuildings() {
simulationData.getBuildings().forEach(b -> {
b.update().ifPresent(c -> addComponentRoute(c, b));
});
}
private void addComponentRoute(Component component, Building fromBuilding) {
@ -95,48 +102,17 @@ public class MainPanel extends JPanel {
.findFirst().get();
Building toBuilding = buildingsById.get(route.getTo());
componentRoutes.add(new ComponentRoute(component, toBuilding, fromBuilding.getX(), fromBuilding.getY()));
Point fromPosition = new Point(fromBuilding.getX(), fromBuilding.getY());
Point toPosition = new Point(toBuilding.getX(), toBuilding.getY());
ComponentIcon icon = new ComponentIcon(component, route.getTo(), fromPosition, toPosition);
componentIcons.add(icon);
add(icon);
setLayer(icon, 1);
}
private void paintRoute(Route route, Graphics g) {
Building fromBuilding = buildingsById.get(route.getFrom());
Image fromIcon = buildingIcons.get(fromBuilding.getType()).get(fromBuilding.getState());
Building toBuilding = buildingsById.get(route.getTo());
Image toIcon = buildingIcons.get(toBuilding.getType()).get(fromBuilding.getState());
g.drawLine(
fromBuilding.getX() + fromIcon.getWidth(null) / 2,
fromBuilding.getY() + fromIcon.getHeight(null) / 2,
toBuilding.getX() + toIcon.getWidth(null) / 2,
toBuilding.getY() + toIcon.getHeight(null) / 2
);
}
private void paintComponentRoute(ComponentRoute route, Graphics g) {
ComponentType componentType = route.getComponent().getType();
Image componentIcon = componentsIcons.get(componentType);
route.updatePosition();
g.drawImage(componentIcon, route.getX(), route.getY(), null);
}
private void loadBuildingImages(BuildingMetadata metadata) throws IOException {
Map<BuildingState, Image> statesIcons = new HashMap<>();
for (BuildingState state : BuildingState.values()) {
statesIcons.put(state, loadBuildingImage(metadata, state));
}
buildingIcons.put(metadata.getType(), statesIcons);
}
private Image loadBuildingImage(BuildingMetadata metadata, BuildingState state) throws IOException {
String iconPath = metadata.getIconsPaths().get(state);
return loadImage(iconPath);
}
private Image loadImage(String path) throws IOException {
return ImageIO.read(new File(path));
private Building getBuildingById(int id) {
return buildingsById.get(id);
}
}

View File

@ -10,7 +10,7 @@ public class MainWindow extends JFrame implements PropertyChangeListener {
@Serial
private static final long serialVersionUID = 1L;
private static final String WINDOW_TITLE = "Laboratoire 1 : LOG121 - Simulation";
private static final Dimension DIMENSION = new Dimension(700, 700);
public static final Dimension DIMENSION = new Dimension(700, 700);
public MainWindow() {
MainPanel mainPanel = new MainPanel();

View File

@ -1,5 +1,11 @@
package view;
import configuration.SimulationConfigurationSingleton;
import simulation.RandomSellStrategy;
import simulation.SellStrategy;
import simulation.TimedSellStrategy;
import simulation.Warehouse;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
@ -11,8 +17,8 @@ public class StrategyPanel extends JPanel {
private static final long serialVersionUID = 1L;
public StrategyPanel() {
JRadioButton strategy1 = new JRadioButton("Stratégie 1");
JRadioButton strategy2 = new JRadioButton("Stratégie 2");
JRadioButton strategy1 = new JRadioButton("Interval");
JRadioButton strategy2 = new JRadioButton("Aléatoire");
ButtonGroup buttonGroup = new ButtonGroup();
buttonGroup.add(strategy1);
@ -20,8 +26,9 @@ public class StrategyPanel extends JPanel {
JButton confirmButton = new JButton("Confirmer");
confirmButton.addActionListener((ActionEvent e) -> {
// TODO - Appeler la bonne stratégie
System.out.println(getSelectedButtonText(buttonGroup));
String selectedStrategyName = getSelectedButtonText(buttonGroup);
onStrategyChange(selectedStrategyName);
// Fermer la fenêtre du component
SwingUtilities.getWindowAncestor((Component) e.getSource()).dispose();
});
@ -54,4 +61,13 @@ public class StrategyPanel extends JPanel {
return null;
}
private void onStrategyChange(String strategyName) {
SellStrategy strategy = strategyName.equalsIgnoreCase("Interval") ?
new TimedSellStrategy() :
new RandomSellStrategy();
Warehouse warehouse = SimulationConfigurationSingleton.getInstance().getConfiguration().getSimulationData().getWarehouse();
warehouse.setSellStrategy(strategy);
}
}

View File

@ -57,7 +57,6 @@ public class WindowMenu extends JMenuBar {
mainPanel.reloadSimulation();
} catch (ConfigurationParsingException ex) {
// TODO WN: Handle exception
throw new RuntimeException("Failed to parse config file", ex);
}
}
@ -82,7 +81,6 @@ public class WindowMenu extends JMenuBar {
chooseMenu.addActionListener((ActionEvent e) -> {
// Ouvrir la fenêtre de sélection
// TODO - Récupérer la bonne stratégie de vente
new StrategyWindow();
});

View File

@ -0,0 +1,81 @@
package view.components;
import metadata.BuildingMetadata;
import simulation.Building;
import simulation.BuildingState;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class BuildingIcon extends JComponent {
private static final Map<String, Map<BuildingState, Image>> loadedImages = new HashMap<>();
private final Building building;
private final Map<BuildingState, Image> images;
public BuildingIcon(Building building) {
this.building = building;
this.images = getImages(building.getMetadata());
setBounds();
}
@Override
protected void paintComponent(Graphics g) {
Image image = getCurrentStateImage();
g.drawImage(image, 0, 0, null);
}
public Image getCurrentStateImage() {
return images.get(building.getState());
}
private void setBounds() {
// Présume que toutes les icônes ont les mêmes dimensions
Image image = images.get(BuildingState.EMPTY);
int x = building.getX();
int y = building.getY();
int width = image.getWidth(null);
int height = image.getHeight(null);
setBounds(x, y, width, height);
}
private static Map<BuildingState, Image> getImages(BuildingMetadata metadata) {
if (loadedImages.containsKey(metadata.getType())) {
return loadedImages.get(metadata.getType());
}
return loadImages(metadata);
}
private static Map<BuildingState, Image> loadImages(BuildingMetadata metadata) {
Map<BuildingState, Image> images = new HashMap<>();
for (BuildingState state : BuildingState.values()) {
String path = metadata.getIconsPaths().get(state);
Image image = loadImage(path);
images.put(state, image);
}
loadedImages.put(metadata.getType(), images);
return images;
}
private static Image loadImage(String path) {
try {
File file = new File(path);
return ImageIO.read(file);
} catch (IOException ex) {
throw new RuntimeException("Could not load building icon", ex);
}
}
}

View File

@ -0,0 +1,91 @@
package view.components;
import simulation.ComponentType;
import simulation.Component;
import view.MainWindow;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class ComponentIcon extends JComponent {
private static final Map<ComponentType, Image> loadedImages = new HashMap<>();
private static final int SPEED = 2;
private final Component component;
private final int destinationId;
private final Point position;
private final Point destination;
private final Point direction;
private final Image image;
public ComponentIcon(Component component, int destinationId, Point source, Point destination) {
this.component = component;
this.destinationId = destinationId;
this.position = source;
this.destination = destination;
this.direction = getDirection();
this.image = getImage(component.getType());
setBounds(0, 0, MainWindow.DIMENSION.width, MainWindow.DIMENSION.height);
}
@Override
public void paintComponent(Graphics g) {
position.translate(direction.x, direction.y);
g.translate(position.x, position.y);
g.drawImage(image, 0, 0, null);
}
public boolean isAtDestination() {
return position.equals(destination);
}
public Component getComponent() {
return component;
}
public int getDestinationId() {
return destinationId;
}
private Point getDirection() {
return new Point(
normalizedDirection(position.x, destination.x),
normalizedDirection(position.y, destination.y)
);
}
private static int normalizedDirection(int from, int to) {
return Integer.compare(to, from) * SPEED;
}
private static Image getImage(ComponentType type) {
if (loadedImages.containsKey(type)) {
return loadedImages.get(type);
}
return loadImage(type);
}
private static Image loadImage(ComponentType type) {
try {
String path = String.format("src/ressources/%s.png", type.getTypeName());
File file = new File(path);
Image image = ImageIO.read(file);
// Garde une référence à l'image chargée pour pouvoir la réutiliser la prochaine fois
loadedImages.put(type, image);
return image;
} catch (IOException ex) {
throw new RuntimeException("Could not load component icon", ex);
}
}
}

View File

@ -0,0 +1,47 @@
package view.components;
import simulation.Building;
import javax.swing.*;
import java.awt.*;
public class RouteLine extends JComponent {
private final Point from;
private final Point to;
public RouteLine(Building fromBuilding, Building toBuilding, int offset) {
from = new Point(fromBuilding.getX(), fromBuilding.getY());
to = new Point(toBuilding.getX(), toBuilding.getY());
setBounds(from, to, offset);
int offsetFromOriginX = Math.min(from.x, to.x);
int offsetFromOriginY = Math.min(from.y, to.y);
from.translate(-offsetFromOriginX, -offsetFromOriginY);
to.translate(-offsetFromOriginX, -offsetFromOriginY);
}
@Override
protected void paintComponent(Graphics g) {
g.drawLine(from.x, from.y, to.x, to.y);
}
private void setBounds(Point from, Point to, int offset) {
int x = Math.min(from.x, to.x);
int y = Math.min(from.y, to.y);
int width = Math.abs(to.x - from.x);
int height = Math.abs(to.y - from.y);
// Si une des dimensions est zéro, la ligne n'apparaîtra pas.
if (width == 0) {
width = 1;
}
if (height == 0) {
height = 1;
}
setBounds(x + offset, y + offset, width, height);
}
}