Compare commits

...

16 Commits

Author SHA1 Message Date
f6d205cd29 hw4: add uml class diagram 2025-04-19 16:36:36 -07:00
573498ddb4 hw4: cleanup 2025-04-19 16:36:31 -07:00
857802227d hw4: initial implementation (gemini 2.5 pro) 2025-04-19 14:46:48 -07:00
34a71d5ef7 midterm2: solve 5.2 - improve 5.1 2025-04-16 23:24:18 -07:00
1be8ca6bf4 midterm2: init 5.1 2025-04-16 22:42:26 -07:00
8dd6533d47 hw4: init 2025-04-15 20:19:52 -07:00
19141831c8 hw3: add googledoc link 2025-03-26 21:41:13 -07:00
f098922149 hw3: add javadoc everywhere 2025-03-26 21:14:46 -07:00
c10ba4b2fc hw3: PhotoAlbumView: improve performance by caching thumbnails 2025-03-25 00:47:27 -07:00
d4705dc11e hw3: PhotoAlbumModel: improve performance by returning unmodifiable list rather than copying 2025-03-25 00:47:19 -07:00
0fb5fd0654 hw3: PhotoAlbumModel: remove sortingStrategy from constructor 2025-03-25 00:32:23 -07:00
b12cf9f6d2 hw3: PhotoAlbumView: add thumbnails, date, file size to list 2025-03-25 00:31:55 -07:00
528254c9bc hw3: PhotoAlbumView: disable unusable buttons 2025-03-25 00:09:22 -07:00
63864e4e25 hw3: use iterator pattern for iterating through list 2025-03-24 23:59:34 -07:00
f8b38bae8f hw3: convert class members to final where possible, make Photo a record class 2025-03-24 23:28:39 -07:00
31302dd0d5 hw3: prototype completed 2025-03-24 23:18:10 -07:00
45 changed files with 1900 additions and 24 deletions

2
hw3/.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="openjdk-22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

1
hw3/googledoc.txt Normal file
View File

@@ -0,0 +1 @@
https://docs.google.com/document/d/1iuiCyrvFca-Ygdt_7QWl_Ie5XfvbfI1GYd8HVLfGGH0/edit?usp=sharing

View File

@@ -1,7 +0,0 @@
public interface AlbumIterator {
boolean hasNext(); // to check if there is a next element
boolean hasPrevious(); // to check if there is a previous element
Photo current(); // to get the photo at the current position
Photo next(); // to advance the iterator to the next position
Photo previous(); // to advance the iterator to the previous position
}

View File

@@ -1,10 +0,0 @@
import java.util.Date;
public class Photo {
private String name;
private String filePath;
private Date dateAdded;
private long fileSize;
// Constructor, getters, and setters
}

View File

@@ -0,0 +1,27 @@
import controller.PhotoAlbumController;
import model.PhotoAlbumModel;
import view.PhotoAlbumView;
/**
* Photo Album Manager application entry point.
* <p>
* This class creates the model, view, and controller objects and initializes the view.
*
* @author Yuri Tatishchev
* @version 0.1 2025-03-26
*/
public class PhotoAlbumApp {
public static void main(String[] args) {
// Create MVC components
PhotoAlbumModel model = new PhotoAlbumModel();
PhotoAlbumView view = new PhotoAlbumView();
PhotoAlbumController controller = new PhotoAlbumController(model, view);
// Initialize view with the controller
view.setController(controller);
// Display the main window
javax.swing.SwingUtilities.invokeLater(() -> view.setVisible(true));
}
}

View File

@@ -1,6 +0,0 @@
import java.util.List;
public interface SortingStrategy<T> {
List<T> sort(List<T> photos);
}

View File

@@ -0,0 +1,135 @@
package controller;
import model.Photo;
import model.PhotoAlbumModel;
import strategy.SortByDate;
import strategy.SortByName;
import strategy.SortBySize;
import view.PhotoAlbumView;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.io.File;
import java.util.Date;
/**
* A controller that manages interactions between the photo album model and view.
* <p>
* This class handles user actions from the view, such as adding and deleting photos,
* navigating through the album, and changing the sorting strategy.
* It acts as an intermediary between the {@link PhotoAlbumModel} and {@link PhotoAlbumView},
* translating user interface events into model operations.
*
* @author Yuri Tatishchev
* @version 0.1 2025-03-26
* @see PhotoAlbumModel
* @see PhotoAlbumView
* @see Photo
*/
public class PhotoAlbumController {
private final PhotoAlbumModel model;
private final PhotoAlbumView view;
/**
* Constructs a new photo album controller.
*
* @param model the photo album model to control
* @param view the view to handle user interactions
*/
public PhotoAlbumController(PhotoAlbumModel model, PhotoAlbumView view) {
this.model = model;
this.view = view;
}
/**
* Returns the photo album model being controlled.
*
* @return the photo album model
*/
public PhotoAlbumModel getModel() {
return model;
}
/**
* Handles the addition of a new photo to the album.
* Opens a file chooser dialog for selecting an image file and
* prompts for a photo name.
*/
public void handleAddPhoto() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileFilter(new FileNameExtensionFilter(
"Image files",
"jpg", "jpeg", "png", "gif"
));
if (fileChooser.showOpenDialog(view) == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
String name = JOptionPane.showInputDialog(view, "Enter photo name:");
if (name != null && !name.trim().isEmpty()) {
Photo photo = new Photo(
name,
file.getAbsolutePath(),
new Date(),
file.length()
);
model.addPhoto(photo);
}
}
}
/**
* Handles the deletion of a photo from the album.
* Prompts for the name of the photo to delete.
*/
public void handleDeletePhoto() {
String name = JOptionPane.showInputDialog(view, "Enter photo name to delete:");
if (name != null && !name.trim().isEmpty()) {
model.deletePhoto(name);
}
}
/**
* Handles navigation to the next photo in the album.
* Moves to the next photo if one exists.
*/
public void handleNext() {
if (model.hasNext()) {
model.next();
}
}
/**
* Handles navigation to the previous photo in the album.
* Moves to the previous photo if one exists.
*/
public void handlePrevious() {
if (model.hasPrevious()) {
model.previous();
}
}
/**
* Handles changing the sorting strategy for photos in the album.
*
* @param strategy the sorting strategy to use:
* <ul>
* <li>0 for name-based sorting,
* <li>1 for date-based sorting,
* <li>2 for size-based sorting
* </ul>
*/
public void handleSort(int strategy) {
switch (strategy) {
case 0:
model.setSortingStrategy(new SortByName());
break;
case 1:
model.setSortingStrategy(new SortByDate());
break;
case 2:
model.setSortingStrategy(new SortBySize());
break;
}
}
}

View File

@@ -0,0 +1,35 @@
package iterator;
import model.Photo;
import java.util.Iterator;
/**
* Interface for album iterators.
*/
public interface AlbumIterator extends Iterator<Photo> {
boolean hasNext(); // to check if there is a next element
/**
* Returns whether there is a previous photo in the album.
*
* @return {@code true} if there is a previous photo, {@code false} otherwise
*/
boolean hasPrevious(); // to check if there is a previous element
/**
* Returns the current photo in the album.
*
* @return the current photo
*/
Photo current(); // to get the photo at the current position
Photo next(); // to advance the iterator to the next position
/**
* Moves to and returns the previous photo in the album.
*
* @return the previous photo
*/
Photo previous(); // to advance the iterator to the previous position
}

View File

@@ -0,0 +1,79 @@
package iterator;
import model.Photo;
import java.util.List;
import java.util.NoSuchElementException;
/**
* An iterator implementation for navigating through photos in a photo album.
* <p>
* This class provides functionality to traverse a list of photos in both
* forward and backward directions. It maintains a current position and
* provides methods to check for the existence of next and previous photos,
* as well as to retrieve the current, next, and previous photos.
* <p>
* The iterator throws {@link NoSuchElementException} when attempting to
* access elements beyond the bounds of the photo list.
*
* @author Yuri Tatishchev
* @version 0.1 2025-03-26
* @see Photo
* @see AlbumIterator
* @see NoSuchElementException
*/
public class PhotoIterator implements AlbumIterator {
private final List<Photo> photos;
private int currentPosition;
/**
* Constructs a new photo iterator for the given list of photos.
* The iterator starts at position 0.
*
* @param photos the list of photos to iterate over
*/
public PhotoIterator(List<Photo> photos) {
this.photos = photos;
this.currentPosition = 0;
}
@Override
public boolean hasNext() {
return currentPosition < photos.size() - 1;
}
@Override
public boolean hasPrevious() {
return currentPosition > 0;
}
@Override
public Photo current() {
if (photos.isEmpty()) {
throw new NoSuchElementException("The photo album is empty.");
}
return photos.get(currentPosition);
}
/**
* @throws NoSuchElementException if there is no next photo
*/
@Override
public Photo next() {
if (!hasNext()) {
throw new NoSuchElementException("No next photo in the album.");
}
return photos.get(++currentPosition);
}
/**
* @throws NoSuchElementException if there is no previous photo
*/
@Override
public Photo previous() {
if (!hasPrevious()) {
throw new NoSuchElementException("No previous photo in the album.");
}
return photos.get(--currentPosition);
}
}

22
hw3/src/model/Photo.java Normal file
View File

@@ -0,0 +1,22 @@
package model;
import java.util.Date;
/**
* A record class that represents a photo.
* <p>
* A photo has a:
* <ul>
* <li>name
* <li>file path
* <li>date added
* <li>file size
* </ul>
* <p>
* A photo is immutable.
*
* @author Yuri Tatishchev
* @version 0.1 2022-03-26
*/
public record Photo(String name, String filePath, Date dateAdded, long fileSize) {
}

View File

@@ -0,0 +1,187 @@
package model;
import strategy.SortingStrategy;
import iterator.AlbumIterator;
import iterator.PhotoIterator;
import java.util.*;
/**
* A model that represents a photo album.
* <p>
* This class is responsible for managing the photos in the album, as well as
* the sorting strategy used to sort the photos.
* The model notifies its listeners when the model changes.
* It also provides methods to add and delete photos,
* as well as to navigate through the photos.
*
* @author Yuri Tatishchev
* @version 0.1 2025-03-26
* @see Photo
* @see SortingStrategy
* @see AlbumIterator
* @see PhotoIterator
* @see ModelChangeListener
*/
public class PhotoAlbumModel {
private List<Photo> photos;
private SortingStrategy<Photo> sortingStrategy;
private final List<ModelChangeListener> listeners;
private AlbumIterator iterator;
/**
* A listener interface for model changes.
* The method {@link #onModelChanged()} is called when the model changes.
*/
public interface ModelChangeListener {
/**
* Called when the model changes.
*/
void onModelChanged();
}
public PhotoAlbumModel() {
photos = new ArrayList<>();
listeners = new ArrayList<>();
iterator = new PhotoIterator(photos);
}
/**
* Adds a photo to the album.
* Resets the iterator and notifies the listeners.
*
* @param photo the photo to add
*/
public void addPhoto(Photo photo) {
photos.add(photo);
sortPhotos();
notifyListeners();
}
/**
* Deletes a photo from the album by name.
* If the deleted photo is the current photo or the album is empty,
* the iterator is reset.
*
* @param name the name of the photo to delete
*/
public void deletePhoto(String name) {
Photo currentPhoto = iterator.current();
photos.removeIf(photo -> photo.name().equals(name));
if (photos.isEmpty() || (currentPhoto != null && currentPhoto.name().equals(name))) {
iterator = new PhotoIterator(photos);
}
notifyListeners();
}
/**
* Sets the sorting strategy for the photos.
* Sorts the photos using the new strategy and resets the iterator.
*
* @param strategy the sorting strategy to set
*/
public void setSortingStrategy(SortingStrategy<Photo> strategy) {
this.sortingStrategy = strategy;
sortPhotos();
iterator = new PhotoIterator(photos);
notifyListeners();
}
private void sortPhotos() {
if (sortingStrategy != null) {
photos = sortingStrategy.sort(photos);
iterator = new PhotoIterator(photos);
}
}
/**
* Adds a listener for model changes.
*
* @param listener the listener to add
*/
public void addListener(ModelChangeListener listener) {
listeners.add(listener);
}
/**
* Notifies all listeners that the model has changed,
* calling the {@link ModelChangeListener#onModelChanged()} method.
*/
private void notifyListeners() {
for (ModelChangeListener listener : listeners) {
listener.onModelChanged();
}
}
/**
* Returns an unmodifiable list of photos in the album.
*
* @return an unmodifiable list of photos
*/
public List<Photo> getPhotos() {
return Collections.unmodifiableList(photos);
}
/**
* Returns the photo in the album that the iterator is currently pointing to.
*
* @return the current photo
*/
public Photo getCurrentPhoto() {
try {
return iterator.current();
} catch (NoSuchElementException e) {
return null;
}
}
/**
* Returns whether there is a next photo in the album.
*
* @return {@code true} if there is a next photo, {@code false} otherwise
*/
public boolean hasNext() {
return iterator.hasNext();
}
/**
* Returns whether there is a previous photo in the album.
*
* @return {@code true} if there is a previous photo, {@code false} otherwise
*/
public boolean hasPrevious() {
return iterator.hasPrevious();
}
/**
* Advances the iterator to the next photo in the album.
* Notifies the listeners.
*
* @return the next photo or {@code null} if there is no next photo
*/
public Photo next() {
try {
Photo next = iterator.next();
notifyListeners();
return next;
} catch (NoSuchElementException e) {
return null;
}
}
/**
* Advances the iterator to the previous photo in the album.
* Notifies the listeners.
*
* @return the previous photo or {@code null} if there is no previous photo
*/
public Photo previous() {
try {
Photo prev = iterator.previous();
notifyListeners();
return prev;
} catch (NoSuchElementException e) {
return null;
}
}
}

View File

@@ -0,0 +1,17 @@
package strategy;
import model.Photo;
import java.util.Comparator;
import java.util.List;
/**
* A sorting strategy that sorts photos by date.
*/
public class SortByDate implements SortingStrategy<Photo> {
@Override
public List<Photo> sort(List<Photo> photos) {
photos.sort(Comparator.comparing(Photo::dateAdded));
return photos;
}
}

View File

@@ -0,0 +1,17 @@
package strategy;
import model.Photo;
import java.util.Comparator;
import java.util.List;
/**
* A sorting strategy that sorts photos by name.
*/
public class SortByName implements SortingStrategy<Photo> {
@Override
public List<Photo> sort(List<Photo> photos) {
photos.sort(Comparator.comparing(Photo::name));
return photos;
}
}

View File

@@ -0,0 +1,17 @@
package strategy;
import model.Photo;
import java.util.Comparator;
import java.util.List;
/**
* A sorting strategy that sorts photos by size.
*/
public class SortBySize implements SortingStrategy<Photo> {
@Override
public List<Photo> sort(List<Photo> photos) {
photos.sort(Comparator.comparing(Photo::fileSize));
return photos;
}
}

View File

@@ -0,0 +1,12 @@
package strategy;
import java.util.List;
/**
* Interface for sorting strategies.
*
* @param <T> the type of elements to be sorted
*/
public interface SortingStrategy<T> {
List<T> sort(List<T> photos);
}

View File

@@ -0,0 +1,318 @@
package view;
import controller.PhotoAlbumController;
import model.Photo;
import model.PhotoAlbumModel;
import javax.swing.*;
import java.awt.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A view that displays a photo album user interface.
* <p>
* This class is responsible for displaying the photo album's graphical interface,
* including the list of photos, current photo display, and control buttons.
* The view implements the {@link PhotoAlbumModel.ModelChangeListener} interface
* to receive notifications of model changes.
* <p>
* The view provides user interface components for:
* <ul>
* <li>Displaying a list of photos with thumbnails and details
* <li>Showing the currently selected photo
* <li>Navigation controls (previous/next)
* <li>Photo management (add/delete)
* <li>Sorting options
* </ul>
*
* @author Yuri Tatishchev
* @version 0.1 2025-03-26
* @see PhotoAlbumModel
* @see PhotoAlbumController
* @see Photo
*/
public class PhotoAlbumView extends JFrame implements PhotoAlbumModel.ModelChangeListener {
private PhotoAlbumModel model;
private JList<String> photoList;
private DefaultListModel<String> listModel;
private JLabel currentPhotoLabel;
private JButton addButton;
private JButton deleteButton;
private JButton previousButton;
private JButton nextButton;
private JComboBox<String> sortingCombo;
public PhotoAlbumView() {
initializeComponents();
setupLayout();
}
/**
* Initializes all UI components of the photo album view.
* <p>
* Sets up the frame properties, creates buttons, list components,
* and initializes them with default states.
*/
private void initializeComponents() {
setTitle("Photo Album Manager");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(800, 600);
listModel = new DefaultListModel<>();
photoList = new JList<>(listModel);
currentPhotoLabel = new JLabel("No photo selected", SwingConstants.CENTER);
addButton = new JButton("Add Photo");
deleteButton = new JButton("Delete Photo");
previousButton = new JButton("Previous");
nextButton = new JButton("Next");
// Disable navigation buttons by default
previousButton.setEnabled(false);
nextButton.setEnabled(false);
deleteButton.setEnabled(false);
String[] sortOptions = {"Sort by Name", "Sort by Date", "Sort by Size"};
sortingCombo = new JComboBox<>(sortOptions);
listModel = new DefaultListModel<>();
photoList = new JList<>(listModel);
photoList.setEnabled(false);
photoList.setCellRenderer(new PhotoListCellRenderer());
photoList.setFixedCellHeight(60); // Accommodate thumbnails
}
/**
* Sets up the layout of the photo album view.
* <p>
* Arranges the UI components using BorderLayout with:
* <ul>
* <li>Photo list in the WEST
* <li>Current photo display in the CENTER
* <li>Control buttons in the SOUTH
* </ul>
*/
private void setupLayout() {
setLayout(new BorderLayout());
// Left panel with photo list
JScrollPane listScrollPane = new JScrollPane(photoList);
listScrollPane.setPreferredSize(new Dimension(200, 0));
add(listScrollPane, BorderLayout.WEST);
// Center panel with current photo
JPanel centerPanel = new JPanel(new BorderLayout());
centerPanel.add(currentPhotoLabel, BorderLayout.CENTER);
add(centerPanel, BorderLayout.CENTER);
// Bottom panel with controls
JPanel controlPanel = new JPanel();
controlPanel.add(previousButton);
controlPanel.add(nextButton);
controlPanel.add(addButton);
controlPanel.add(deleteButton);
controlPanel.add(sortingCombo);
add(controlPanel, BorderLayout.SOUTH);
}
/**
* Sets the controller for the photo album view.
* <p>
* Attaches the controller to the view and sets up event listeners
* for the control buttons and sorting combo box.
*
* @param controller the controller to set
*/
public void setController(PhotoAlbumController controller) {
this.model = controller.getModel();
model.addListener(this);
addButton.addActionListener(e -> controller.handleAddPhoto());
deleteButton.addActionListener(e -> controller.handleDeletePhoto());
nextButton.addActionListener(e -> controller.handleNext());
previousButton.addActionListener(e -> controller.handlePrevious());
sortingCombo.addActionListener(e -> controller.handleSort(sortingCombo.getSelectedIndex()));
// Select default sorting option
sortingCombo.setSelectedIndex(1);
}
@Override
public void onModelChanged() {
updatePhotoList();
updateCurrentPhoto();
updateNavigationButtons();
}
/**
* Updates the list of photos displayed in the sidebar.
* <p>
* Clears the current list model and populates it with
* names of photos from the model.
*/
private void updatePhotoList() {
listModel.clear();
List<Photo> photos = model.getPhotos();
for (Photo photo : photos) {
listModel.addElement(photo.name());
}
}
/**
* Updates the display of the current photo in the main view area.
* <p>
* If a photo is selected, loads and displays its image.
* If loading fails, displays an error message.
* If no photo is selected, displays a default message.
*/
private void updateCurrentPhoto() {
Photo current = model.getCurrentPhoto();
if (current != null) {
ImageIcon icon = loadImage(current.filePath());
if (icon != null) {
currentPhotoLabel.setIcon(icon);
currentPhotoLabel.setText("");
} else {
currentPhotoLabel.setIcon(null);
currentPhotoLabel.setText("Unable to load image");
}
} else {
currentPhotoLabel.setIcon(null);
currentPhotoLabel.setText("No photo selected");
}
}
/**
* Updates the enabled state of navigation buttons.
* <p>
* Enables or disables the previous/next buttons based on
* the current position in the photo album.
* Enables the delete button only when the album is not empty.
*/
private void updateNavigationButtons() {
previousButton.setEnabled(model.hasPrevious());
nextButton.setEnabled(model.hasNext());
deleteButton.setEnabled(model.getCurrentPhoto() != null);
}
/**
* Loads and scales an image from the given path.
* <p>
* Creates a scaled version of the image suitable for the main display area.
*
* @param path the file path of the image to load
* @return a scaled ImageIcon, or null if loading fails
*/
private ImageIcon loadImage(String path) {
try {
ImageIcon icon = new ImageIcon(path);
Image img = icon.getImage();
Image scaledImg = img.getScaledInstance(400, 300, Image.SCALE_SMOOTH);
return new ImageIcon(scaledImg);
} catch (Exception e) {
return null;
}
}
/**
* A custom list cell renderer for displaying photos with thumbnails and details.
* <p>
* This inner class renders each photo in the list with:
* <ul>
* <li>A thumbnail image
* <li>The photo name
* <li>Additional details (date and file size)
* </ul>
* <p>
* It also implements thumbnail caching to improve performance.
*/
private class PhotoListCellRenderer extends JPanel implements ListCellRenderer<String> {
private final JLabel imageLabel = new JLabel();
private final JLabel textLabel = new JLabel();
private final JLabel detailsLabel = new JLabel();
private final Map<String, ImageIcon> thumbnailCache = new HashMap<>();
public PhotoListCellRenderer() {
setLayout(new BorderLayout(5, 0));
// Left side - image
imageLabel.setPreferredSize(new Dimension(50, 50));
add(imageLabel, BorderLayout.WEST);
// Center - name and details
JPanel textPanel = new JPanel(new GridLayout(2, 1));
textPanel.setOpaque(false);
textLabel.setFont(textLabel.getFont().deriveFont(Font.BOLD));
detailsLabel.setForeground(Color.GRAY);
detailsLabel.setFont(detailsLabel.getFont().deriveFont(10.0f));
textPanel.add(textLabel);
textPanel.add(detailsLabel);
add(textPanel, BorderLayout.CENTER);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
setOpaque(true);
}
@Override
public Component getListCellRendererComponent(JList<? extends String> list,
String name,
int index,
boolean isSelected,
boolean cellHasFocus
) {
textLabel.setText(name);
Photo photo = model.getPhotos().get(index);
ImageIcon thumbnail = thumbnailCache.computeIfAbsent(photo.filePath(), this::loadThumbnail);
// ImageIcon thumbnail = loadThumbnail(photo.filePath());
imageLabel.setIcon(thumbnail);
// Format date
String date = String.format("%tF", photo.dateAdded());
// Format file size
String size = formatFileSize(photo.fileSize());
detailsLabel.setText(date + "" + size);
return this;
}
/**
* Formats a file size in bytes to a human-readable string.
* <p>
* Converts the size to kilobytes or megabytes with one decimal place.
*
* @param size the file size in bytes
* @return a formatted string with the size in KB or MB
*/
private String formatFileSize(long size) {
if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else {
return String.format("%.1f MB", size / (1024.0 * 1024));
}
}
/**
* Loads and scales a thumbnail image from the given path.
* <p>
* Creates a scaled version of the image suitable for the list display.
*
* @param path the file path of the image to load
* @return a scaled ImageIcon, or null if loading fails
*/
private ImageIcon loadThumbnail(String path) {
try {
ImageIcon icon = new ImageIcon(path);
Image img = icon.getImage();
Image scaledImg = img.getScaledInstance(50, 50, Image.SCALE_AREA_AVERAGING);
return new ImageIcon(scaledImg);
} catch (Exception e) {
return null;
}
}
}
}

29
hw4/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
### IntelliJ IDEA ###
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
hw4/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
hw4/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="openjdk-22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
hw4/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/hw4.iml" filepath="$PROJECT_DIR$/hw4.iml" />
</modules>
</component>
</project>

6
hw4/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

24
hw4/README.md Normal file
View File

@@ -0,0 +1,24 @@
# Programming Assignment 4 - Shape Displayer
## Requirements
The user interacts with the application as follows:
- Initially, the program displays three buttons on the top of the frame and a
blank large area of a user-defined JPanel below the buttons. The buttons
display a car, a snow man, and a composite shape of your choice, respectively.
You are allowed to borrow the CarShape.java from the text book. Make sure to
acknowledge that the code is from the text book. The composite shape of your
choice should consist of at least 4 primitive shapes, but doesn't have to be
fancy.
- When the user clicks on one of the buttons, the shape displayed on the clicked
button becomes the current shape. The icon representing the current shape
should outline the shape as shown below in the sample output. When a mouse is
clicked on the user-defined JPanel, the current shape is drawn on the position
which the mouse was pressed on.
- The current shape can be changed by clicking one of the buttons.
- The application should be reusable by any CompositeShape.
- Your design should be object-oriented including ShapeDisplayer(with main
method), CarShape, SnowMan, and Your_own_shape classes. Also, it should
include interface CompositeShape.java and a concrete class ShapeIcon. You may
include more classes to your design.

BIN
hw4/diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

80
hw4/diagram.uml Normal file
View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>JAVA</ID>
<OriginalElement />
<nodes>
<node x="-90.5" y="176.0">ShapeIcon</node>
<node x="-76.0" y="-4.0">CompositeShape</node>
<node x="-50.0" y="296.0">ShapeDisplayer</node>
<node x="12.5" y="116.0">CarShape</node>
<node x="208.0" y="116.0">SnowmanShape</node>
<node x="101.5" y="176.0">HouseShape</node>
</nodes>
<notes />
<edges>
<edge source="CarShape" target="CompositeShape" relationship="REALIZATION">
<point x="62.5" y="0.0" />
<point x="173.0" y="130.0" />
<point x="173.0" y="10.0" />
<point x="91.0" y="0.0" />
</edge>
<edge source="ShapeDisplayer" target="CompositeShape" relationship="TO_ONE">
<point x="-2.0" y="-14.0" />
<point x="33.0" y="221.5" />
<point x="-108.0" y="221.5" />
<point x="-108.0" y="10.0" />
<point x="-91.0" y="0.0" />
</edge>
<edge source="ShapeDisplayer" target="SnowmanShape" relationship="CREATE">
<point x="85.0" y="0.0" />
<point x="295.0" y="310.0" />
<point x="0.0" y="14.0" />
</edge>
<edge source="ShapeDisplayer" target="HouseShape" relationship="CREATE">
<point x="77.5" y="-14.0" />
<point x="-62.5" y="14.0" />
</edge>
<edge source="ShapeIcon" target="CompositeShape" relationship="TO_ONE">
<point x="98.125" y="-0.08333333333334281" />
<point x="-298.1666666666667" y="189.91666666666666" />
<point x="-298.1666666666667" y="212.33333333333337" />
<point x="-108.0" y="212.33333333333337" />
<point x="-108.0" y="10.0" />
<point x="-91.0" y="0.0" />
</edge>
<edge source="SnowmanShape" target="CompositeShape" relationship="REALIZATION">
<point x="-87.0" y="0.0" />
<point x="173.0" y="130.0" />
<point x="173.0" y="10.0" />
<point x="91.0" y="0.0" />
</edge>
<edge source="ShapeDisplayer" target="ShapeIcon" relationship="TO_ONE">
<point x="-60.0" y="-14.0" />
<point x="0.0" y="14.0" />
</edge>
<edge source="ShapeDisplayer" target="CarShape" relationship="CREATE">
<point x="-85.0" y="0.0" />
<point x="-123.0" y="310.0" />
<point x="-123.0" y="130.0" />
<point x="-62.5" y="0.0" />
</edge>
<edge source="ShapeDisplayer" target="ShapeIcon" relationship="CREATE">
<point x="-45.0" y="-14.0" />
<point x="15.0" y="14.0" />
</edge>
<edge source="HouseShape" target="CompositeShape" relationship="REALIZATION">
<point x="73.5" y="0.0" />
<point x="399.5" y="190.0" />
<point x="399.5" y="60.5" />
<point x="173.0" y="60.5" />
<point x="173.0" y="10.0" />
<point x="91.0" y="0.0" />
</edge>
</edges>
<settings layout="Hierarchic Compact" zoom="1.2" showDependencies="true" x="233.54166666666663" y="134.91666666666666" />
<SelectedNodes />
<Categories />
<SCOPE>All</SCOPE>
<VISIBILITY>private</VISIBILITY>
</Diagram>

11
hw4/hw4.iml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

77
hw4/src/CarShape.java Normal file
View File

@@ -0,0 +1,77 @@
import java.awt.*;
import java.awt.geom.*;
/**
* A car shape.
* Partially taken from the textbook (Cay S. Horstmann - OO Design & Patterns, 2nd ed.)
*/
public class CarShape implements CompositeShape {
private final int x; // Base x-coordinate (relative to drawing origin)
private final int y; // Base y-coordinate (relative to drawing origin)
private final int width;
public CarShape() {
this(0, 0, 60); // Default position and width
}
/**
* Constructs a car shape.
*
* @param x the left of the bounding rectangle
* @param y the top of the bounding rectangle
* @param width the width of the bounding rectangle
*/
public CarShape(int x, int y, int width) {
this.x = x;
this.y = y;
this.width = width;
}
@Override
public int getWidth() {
return width; // Width of the car body
}
@Override
public int getHeight() {
// Approximate height: body height + wheel radius
return width / 2 + width / 6;
}
@Override
public void draw(Graphics2D g2) {
Rectangle2D.Double body = new Rectangle2D.Double(x, y + width / 6.0, width - 1, width / 6.0);
Ellipse2D.Double frontTire = new Ellipse2D.Double(
x + width / 6.0,
y + width / 3.0,
width / 6.0,
width / 6.0
);
Ellipse2D.Double rearTire = new Ellipse2D.Double(
x + width * 2 / 3.0,
y + width / 3.0,
width / 6.0,
width / 6.0
);
// The bottom of the front windshield
Point2D.Double r1 = new Point2D.Double(x + width / 6.0, y + width / 6.0);
// The front of the roof
Point2D.Double r2 = new Point2D.Double(x + width / 3.0, y);
// The rear of the roof
Point2D.Double r3 = new Point2D.Double(x + width * 2 / 3.0, y);
// The bottom of the rear windshield
Point2D.Double r4 = new Point2D.Double(x + width * 5 / 6.0, y + width / 6.0);
Line2D.Double frontWindshield = new Line2D.Double(r1, r2);
Line2D.Double roofTop = new Line2D.Double(r2, r3);
Line2D.Double rearWindshield = new Line2D.Double(r3, r4);
// Draw the parts
g2.draw(body);
g2.draw(frontTire);
g2.draw(rearTire);
g2.draw(frontWindshield);
g2.draw(roofTop);
g2.draw(rearWindshield);
}
}

View File

@@ -0,0 +1,27 @@
import java.awt.Graphics2D;
/**
* An interface for shapes that can be composed of multiple primitive shapes
* and can be drawn.
*/
public interface CompositeShape {
/**
* Draws the shape.
* @param g2 the graphics context
*/
void draw(Graphics2D g2);
/**
* Gets the width of the shape's bounding box.
* Used for creating icons of a consistent size.
* @return the width
*/
int getWidth();
/**
* Gets the height of the shape's bounding box.
* Used for creating icons of a consistent size.
* @return the height
*/
int getHeight();
}

68
hw4/src/HouseShape.java Normal file
View File

@@ -0,0 +1,68 @@
import java.awt.Graphics2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
/**
* A simple house shape composed of a body (rectangle), roof (triangle),
* door (rectangle), and window (rectangle).
*/
public class HouseShape implements CompositeShape {
private final int width; // Width of the house body
private final int height; // Height of the house body (excluding roof)
public HouseShape() {
this(50, 40); // Default width and body height
}
public HouseShape(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
// Total height = body height + roof height (approx 0.5 * width)
return height + (int) (width * 0.5);
}
@Override
public void draw(Graphics2D g2) {
// Drawing relative to (0,0) - top-left of bounding box.
// Body
double bodyY = width * 0.5; // Start body below roof peak
Rectangle2D.Double body = new Rectangle2D.Double(0, bodyY, width, height);
// Roof (triangle using Path2D)
Path2D.Double roof = new Path2D.Double();
roof.moveTo(0, bodyY); // Bottom-left corner of roof
roof.lineTo(width / 2.0, 0); // Peak of roof
roof.lineTo(width, bodyY); // Bottom-right corner of roof
// No need to close path for drawing outline
// Door (small rectangle)
double doorWidth = width * 0.25;
double doorHeight = height * 0.5;
double doorX = (width - doorWidth) / 2.0; // Centered horizontally
double doorY = bodyY + height - doorHeight; // At the bottom of the body
Rectangle2D.Double door = new Rectangle2D.Double(doorX, doorY, doorWidth, doorHeight);
// Window (small square rectangle)
double windowSize = width * 0.2;
double windowX = width * 0.15; // Positioned left of center
double windowY = bodyY + height * 0.2; // Positioned near top of body
Rectangle2D.Double window = new Rectangle2D.Double(windowX, windowY, windowSize, windowSize);
// Draw parts
g2.draw(body);
g2.draw(roof);
g2.draw(door);
g2.draw(window);
}
}

187
hw4/src/ShapeDisplayer.java Normal file
View File

@@ -0,0 +1,187 @@
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.List;
public class ShapeDisplayer extends JFrame {
private static final int ICON_WIDTH = 50;
private static final int ICON_HEIGHT = 50;
private final DrawingPanel drawingPanel;
private CompositeShape currentShape = null; // The shape currently selected
private final JButton carButton;
private final JButton snowmanButton;
private final JButton houseButton;
private final ShapeIcon carIcon;
private final ShapeIcon snowmanIcon;
private final ShapeIcon houseIcon;
// Helper class to store drawn shapes and their positions
private static class PlacedShape {
final CompositeShape shape;
final Point position; // Top-left corner where the shape should be drawn
PlacedShape(CompositeShape shape, Point position) {
this.shape = shape;
this.position = position;
}
}
// Custom JPanel for drawing
private class DrawingPanel extends JPanel {
private final List<PlacedShape> drawnShapes = new ArrayList<>();
public DrawingPanel() {
setBackground(Color.WHITE);
setPreferredSize(new Dimension(600, 400)); // Set preferred size
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (currentShape != null) {
// Create a *new instance* for drawing, if shapes had state.
// For these stateless shapes, reusing is fine, but creating
// new instances is conceptually safer if shapes could change.
// Example: drawnShapes.add(new PlacedShape(createNewShapeInstance(currentShape), e.getPoint()));
// Simpler approach for current stateless shapes:
drawnShapes.add(new PlacedShape(currentShape, e.getPoint()));
repaint(); // Trigger redraw
}
}
});
}
public void setCurrentShape(CompositeShape shape) {
currentShape = shape;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create(); // Work on a copy
// Enable antialiasing for smoother graphics
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Draw all the placed shapes
for (PlacedShape ps : drawnShapes) {
// Save the current transform state
AffineTransform savedTransform = g2.getTransform();
// Translate to the position where the mouse was clicked
g2.translate(ps.position.x, ps.position.y);
// Draw the shape (its draw method assumes drawing at 0,0)
ps.shape.draw(g2);
// Restore the original transform for the next shape
g2.setTransform(savedTransform);
}
g2.dispose(); // Clean up the graphics copy
}
}
public ShapeDisplayer() {
super("Shape Displayer"); // Frame title
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// --- Create Shapes ---
CompositeShape car = new CarShape();
CompositeShape snowman = new SnowmanShape();
CompositeShape house = new HouseShape(); // Your custom shape
// --- Create Icons ---
carIcon = new ShapeIcon(car, ICON_WIDTH, ICON_HEIGHT);
snowmanIcon = new ShapeIcon(snowman, ICON_WIDTH, ICON_HEIGHT);
houseIcon = new ShapeIcon(house, ICON_WIDTH, ICON_HEIGHT);
// --- Create Buttons ---
carButton = new JButton(carIcon);
snowmanButton = new JButton(snowmanIcon);
houseButton = new JButton(houseIcon);
// Configure buttons (remove text, set margins if needed)
carButton.setMargin(new Insets(2, 2, 2, 2));
snowmanButton.setMargin(new Insets(2, 2, 2, 2));
houseButton.setMargin(new Insets(2, 2, 2, 2));
// --- Create Panels ---
drawingPanel = new DrawingPanel();
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); // Align buttons left
buttonPanel.add(carButton);
buttonPanel.add(snowmanButton);
buttonPanel.add(houseButton);
// --- Layout ---
setLayout(new BorderLayout());
add(buttonPanel, BorderLayout.NORTH);
add(drawingPanel, BorderLayout.CENTER);
// --- Add Action Listeners ---
carButton.addActionListener(e -> selectShape(car, carIcon));
snowmanButton.addActionListener(e -> selectShape(snowman, snowmanIcon));
houseButton.addActionListener(e -> selectShape(house, houseIcon));
// Initially, no shape is selected
deselectAllIcons();
pack(); // Size the frame based on components
setLocationRelativeTo(null); // Center on screen
}
// Method to handle shape selection
private void selectShape(CompositeShape shape, ShapeIcon icon) {
drawingPanel.setCurrentShape(shape);
deselectAllIcons(); // Clear previous selection outline
icon.setOutline(true); // Set outline on the selected icon
// Force repaint of the button containing the icon
if (icon == carIcon) carButton.repaint();
else if (icon == snowmanIcon) snowmanButton.repaint();
else if (icon == houseIcon) houseButton.repaint();
// Optional: Add a border to the button itself for clearer selection
// resetBorders(); // Reset borders on all buttons
// if (icon == carIcon) carButton.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
// else if (icon == snowManIcon) snowManButton.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
// else if (icon == houseIcon) houseButton.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
}
// Method to remove outline from all icons
private void deselectAllIcons() {
carIcon.setOutline(false);
snowmanIcon.setOutline(false);
houseIcon.setOutline(false);
// Repaint all buttons to remove outlines
carButton.repaint();
snowmanButton.repaint();
houseButton.repaint();
}
// Optional: Helper to reset button borders
// private void resetBorders() {
// Border defaultBorder = UIManager.getBorder("Button.border");
// carButton.setBorder(defaultBorder);
// snowManButton.setBorder(defaultBorder);
// houseButton.setBorder(defaultBorder);
// }
public static void main(String[] args) {
// Run the GUI creation on the Event Dispatch Thread (EDT)
SwingUtilities.invokeLater(() -> {
ShapeDisplayer displayer = new ShapeDisplayer();
displayer.setVisible(true);
});
}
}

90
hw4/src/ShapeIcon.java Normal file
View File

@@ -0,0 +1,90 @@
import javax.swing.Icon;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
/**
* An icon that draws a CompositeShape.
*/
public class ShapeIcon implements Icon {
private final CompositeShape shape;
private final int width;
private final int height;
private boolean outline = false; // Flag to indicate if the icon should be outlined
/**
* Constructs a ShapeIcon.
* @param shape the shape to draw
* @param width the desired width of the icon
* @param height the desired height of the icon
*/
public ShapeIcon(CompositeShape shape, int width, int height) {
this.shape = shape;
this.width = width;
this.height = height;
}
/**
* Sets whether this icon should draw an outline (e.g., when selected).
* @param outline true to draw outline, false otherwise.
*/
public void setOutline(boolean outline) {
this.outline = outline;
}
@Override
public int getIconWidth() {
return width;
}
@Override
public int getIconHeight() {
return height;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g.create(); // Work on a copy
// Enable antialiasing for smoother graphics
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Translate to the icon's drawing position
g2.translate(x, y);
double shapeWidth = shape.getWidth();
double shapeHeight = shape.getHeight();
// Calculate scaling factor to fit shape within icon bounds
double scaleX = (shapeWidth == 0) ? 1 : (double) width / shapeWidth;
double scaleY = (shapeHeight == 0) ? 1 : (double) height / shapeHeight;
double scale = Math.min(scaleX, scaleY) * 0.8; // Use 80% of space for padding
// Calculate translation to center the scaled shape
double dx = (width - shapeWidth * scale) / 2.0;
double dy = (height - shapeHeight * scale) / 2.0;
// Apply centering translation and scaling
AffineTransform originalTransform = g2.getTransform();
g2.translate(dx, dy);
g2.scale(scale, scale);
// Draw the shape
shape.draw(g2);
// Restore original transform before drawing outline (if any)
g2.setTransform(originalTransform);
// Draw outline if selected
if (outline) {
// Simple rectangle outline around the icon area
g2.setColor(java.awt.Color.GRAY);
g2.drawRect(0, 0, width - 1, height - 1);
g2.drawRect(1, 1, width - 3, height - 3); // Inner outline
}
g2.dispose(); // Clean up the graphics copy
}
}

60
hw4/src/SnowmanShape.java Normal file
View File

@@ -0,0 +1,60 @@
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
/**
* A snowman shape composed of three circles.
*/
public class SnowmanShape implements CompositeShape {
private final int width; // Base diameter of the bottom circle
public SnowmanShape() {
this(40); // Default width
}
public SnowmanShape(int width) {
this.width = width;
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
// Total height is approx sum of diameters: w + 0.75w + 0.5w
return (int) (width + width * 0.75 + width * 0.5);
}
@Override
public void draw(Graphics2D g2) {
// All drawing relative to (0,0) which represents the top-left
// of the bounding box for the *icon*. The actual drawing will be centered.
// Calculate diameters
double bottomDiameter = width;
double middleDiameter = width * 0.75;
double topDiameter = width * 0.5;
// Calculate positions (y-coordinates are offset from the top)
double bottomY = topDiameter + middleDiameter;
double middleY = topDiameter;
double topY = 0;
// Center horizontally (x-coordinates)
double bottomX = (width - bottomDiameter) / 2.0;
double middleX = (width - middleDiameter) / 2.0;
double topX = (width - topDiameter) / 2.0;
// Create circles
Ellipse2D.Double bottomCircle = new Ellipse2D.Double(bottomX, bottomY, bottomDiameter, bottomDiameter);
Ellipse2D.Double middleCircle = new Ellipse2D.Double(middleX, middleY, middleDiameter, middleDiameter);
Ellipse2D.Double topCircle = new Ellipse2D.Double(topX, topY, topDiameter, topDiameter);
// Draw circles
g2.draw(bottomCircle);
g2.draw(middleCircle);
g2.draw(topCircle);
}
}

29
midterm2/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
### IntelliJ IDEA ###
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
midterm2/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
midterm2/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="openjdk-22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
midterm2/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/midterm2.iml" filepath="$PROJECT_DIR$/midterm2.iml" />
</modules>
</component>
</project>

6
midterm2/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

11
midterm2/midterm2.iml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,96 @@
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;
/**
* A class that implements an Observer object that displays a barchart view of
* a data model.
*/
public class BarFrame extends JFrame implements ChangeListener {
/**
* Constructs a BarFrame object
*
* @param dataModel the data that is displayed in the barchart
*/
public BarFrame(DataModel dataModel) {
this.dataModel = dataModel;
a = dataModel.getData();
setLocation(0, 200);
setLayout(new BorderLayout());
JLabel barLabel = new JLabel();
barLabel.setIcon(new BarIcon());
barLabel.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
int mouseX = e.getX();
int mouseY = e.getY();
double max = a.stream().max(Double::compare).orElse(1.0);
double barHeight = ICON_HEIGHT / (double) a.size();
// Find the nearest bar to the mouse click
int index = (int) (mouseY / barHeight);
if (index >= 0 && index < a.size()) {
double newValue = max * mouseX / ICON_WIDTH;
dataModel.update(index, newValue);
}
}
});
add(barLabel);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setVisible(true);
}
/**
* Called when the data in the model is changed.
*
* @param e the event representing the change
*/
public void stateChanged(ChangeEvent e) {
a = dataModel.getData();
repaint();
}
private class BarIcon implements Icon {
public int getIconWidth() {
return ICON_WIDTH;
}
public int getIconHeight() {
return ICON_HEIGHT;
}
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.red);
double max = a.stream().max(Double::compare).orElse(1.0);
double barHeight = getIconHeight() / (double) a.size();
int i = 0;
for (Double value : a) {
double barLength = getIconWidth() * value / max;
Rectangle2D.Double rectangle = new Rectangle2D.Double(
0, barHeight * i, barLength, barHeight);
i++;
g2.fill(rectangle);
}
}
}
private ArrayList<Double> a;
private DataModel dataModel;
private static final int ICON_WIDTH = 200;
private static final int ICON_HEIGHT = 200;
}

View File

@@ -0,0 +1,51 @@
import java.util.ArrayList;
import javax.swing.event.*;
/**
* A Subject class for the observer pattern.
*/
public class DataModel {
/**
* Constructs a DataModel object
*
* @param d the data to model
*/
public DataModel(ArrayList<Double> d) {
data = d;
listeners = new ArrayList<ChangeListener>();
}
/**
* Constructs a DataModel object
*
* @return the data in an ArrayList
*/
public ArrayList<Double> getData() {
return (ArrayList<Double>) (data.clone());
}
/**
* Attach a listener to the Model
*
* @param c the listener
*/
public void attach(ChangeListener c) {
listeners.add(c);
}
/**
* Change the data in the model at a particular location
*
* @param location the index of the field to change
* @param value the new value
*/
public void update(int location, double value) {
data.set(location, new Double(value));
for (ChangeListener l : listeners) {
l.stateChanged(new ChangeEvent(this));
}
}
ArrayList<Double> data;
ArrayList<ChangeListener> listeners;
}

14
midterm2/src/Main.java Normal file
View File

@@ -0,0 +1,14 @@
/**
* Write a program that contains two frames, one with a column of text fields
* containing numbers, and another that draws a bar graph showing the values of
* the numbers. When the user edits one of the numbers, the graph should be
* redrawn. Use the observer pattern. Store the data in a model. Attach the graph
* view as a listener. When a number is updated, the number view should update the
* model, and the model should tell the graph view that a change has occured. As a
* result, the graph view should repaint itself.
*/
public class Main {
public static void main(String[] args) {
}
}

View File

@@ -0,0 +1,29 @@
import java.util.ArrayList;
/**
* A class for testing an implementation of the Observer pattern.
*/
public class ObserverTester {
/**
* Creates a DataModel and attaches barchart and textfield listeners
*
* @param args unused
*/
public static void main(String[] args) {
ArrayList<Double> data = new ArrayList<Double>();
data.add(33.0);
data.add(44.0);
data.add(22.0);
data.add(22.0);
DataModel model = new DataModel(data);
TextFrame frame = new TextFrame(model);
BarFrame barFrame = new BarFrame(model);
model.attach(frame);
model.attach(barFrame);
}
}

View File

@@ -0,0 +1,76 @@
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.util.ArrayList;
/**
* A class for displaying the model as a column of textfields in a frame.
*/
public class TextFrame extends JFrame implements ChangeListener {
/**
* Constructs a JFrame that contains the textfields containing the data
* in the model.
*
* @param d the model to display
*/
public TextFrame(DataModel d) {
dataModel = d;
final Container contentPane = this.getContentPane();
setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));
ArrayList<Double> a = dataModel.getData();
fieldList = new JTextField[a.size()];
// A listener for action events in the text fields
ActionListener l = new ActionListener() {
public void actionPerformed(ActionEvent e) {
// Figure out which field generated the event
JTextField c = (JTextField) e.getSource();
int i = 0;
int count = fieldList.length;
while (i < count && fieldList[i] != c)
i++;
String text = c.getText().trim();
try {
double value = Double.parseDouble(text);
dataModel.update(i, value);
} catch (Exception exc) {
c.setText("Error. No update");
}
}
};
final int FIELD_WIDTH = 11;
for (int i = 0; i < a.size(); i++) {
JTextField textField = new JTextField(a.get(i).toString(), FIELD_WIDTH);
textField.addActionListener(l);
add(textField);
fieldList[i] = textField;
}
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setVisible(true);
}
/**
* Called when the data in the model is changed.
*
* @param e the event representing the change
*/
@Override
public void stateChanged(ChangeEvent e) {
ArrayList<Double> a = dataModel.getData();
for (int i = 0; i < a.size(); i++) {
fieldList[i].setText(a.get(i).toString());
}
}
DataModel dataModel;
JTextField[] fieldList;
}