AttributedItemContainer.java

package org.troy.capstone.ui_components.items;

import java.awt.Desktop;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

import org.troy.capstone.constants.URL;
import org.troy.capstone.annotations.Generated;
import org.troy.capstone.constants.UISizeControl;
import org.troy.capstone.entities.Item;

import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;

/**
 * The {@code AttributedItemContainer} class represents a UI component that displays an item's image along with its attribution information.
 */
public class AttributedItemContainer extends VBox {

    /** The {@code ImageView} for displaying the item's image. */
    private final ImageView imageView;

    /** The task used to load the image asynchronously. */
    private Task<Image> loadImageTask;


    /** The {@code Desktop} instance used to open URLs in the default browser. Used in testing to allow for mocking to disable actual browser opening. */
    @SuppressWarnings("FieldMayBeFinal")
    private static Desktop desktop = Desktop.getDesktop();

    /** Creates an {@code AttributedItemContainer} for the given item, initializing the image view and attribution flow.
     * 
     * @pre item should contain valid data for the image URL and attribution information.
     *      The {@code AttributedItemContainer} should be properly initialized to display the item's image and attribution information.
     *      The {@code loadImageTask} should be not initialized, it will be set up to load the image asynchronously within this constructor.
     * @post The variable passed into {@code loadImageTask} will be initialized to a {@code Task} that loads the image from the item's image URL asynchronously.
     * 
     * @param item The item whose image and attribution information are being displayed in this container.
     */
    public AttributedItemContainer(Item item) {
        super(5); //5px spacing between items
        setAlignment(Pos.TOP_CENTER);

        TextFlow attributionFlow = makeAttributionFlow(item);

        imageView = new ImageView();
        imageView.setFitWidth(UISizeControl.ATTRIBUTED_ITEM_IMAGE_WIDTH.getValue());
        imageView.setFitHeight(UISizeControl.ATTRIBUTED_ITEM_IMAGE_HEIGHT.getValue());
        imageView.setPreserveRatio(true);
        
        //Load image asynchronously to avoid blocking scroll
        loadImageTask = loadImageAsync(item.getImageUrl());
        
        imageView.setOnMouseClicked(e -> {
            try {
                desktop.browse(new URI(item.getImageUrl()));
            } catch (IOException | URISyntaxException ex) {
                System.err.println("Failed to open image URL: " + item.getImageUrl());
            }
        });
        
        //Optimize rendering
        setCache(true);
        setCacheHint(javafx.scene.CacheHint.SPEED);

        getChildren().addAll(imageView, attributionFlow);
    }

    /** Stops the asynchronous loading of the image in this {@code AttributedItemContainer}. This method can be called when the container is no longer visible or needed, to free up resources and prevent unnecessary loading of images that are not being displayed. 
     * @post The asynchronous image loading task for this {@code AttributedItemContainer} is stopped, preventing any further loading of the image that is not being displayed.
    */
    @Generated
    public void stopLoadingImage() {
        if (loadImageTask != null && loadImageTask.isRunning())
            loadImageTask.cancel();
    }

    /**
     * Creates a {@code TextFlow} for the attribution text with clickable links for the author and source.
     * 
     * @pre item should contain valid data for the photo author and their URL, as well as the source URL for Unsplash.
     * 
     * @param item The item whose data is being used to create the attribution flow, specifically the photo author and their URL.
     * @return A {@code TextFlow} containing the attribution text with clickable links for the author and source.
     */
    @SuppressWarnings("FinalPrivateMethod")
    private final TextFlow makeAttributionFlow(Item item) {
        Text text1 = new Text("Photo by ");
        Text authorName = new Text(item.getPhotoAuthor());
        authorName.setUnderline(true);
        Text text2 = new Text(" on ");
        Text sourceName = new Text("Unsplash"); 
        sourceName.setUnderline(true); 

        authorName.setOnMouseClicked(e ->{
            try {
                desktop.browse(new URI(item.getPhotoAuthorUrl()));
            } catch (IOException | URISyntaxException ex) {
                System.err.println("Failed to open author URL: " + item.getPhotoAuthorUrl());
            }
        });
        sourceName.setOnMouseClicked(e ->{
            try {
                desktop.browse( new URI( URL.UNSPLASH_ATTRIBUTION.getUrl() ) );
            } catch (IOException | URISyntaxException ex) {
                System.err.println("Failed to open source URL: " + URL.UNSPLASH_ATTRIBUTION.getUrl());
            }
        });
        
        //Create the TextFlow and add all the text nodes to it
        return new TextFlow(text1, authorName, text2, sourceName);
    }
    
    /**
     * Getter for the {@code ImageView} in the {@code AttributedItemContainer}, which displays the item's image.
     * @return The {@code ImageView} displaying the item's image in the {@code AttributedItemContainer}.
     */
    public ImageView getImageView() {
        return imageView;
    }

    /**
     * Loads an image from a URL asynchronously to avoid blocking the UI thread,
     *  and sets it to the imageView once loaded.
     * 
     * @post The image from the specified URL will be loaded and displayed in the imageView of the {@code AttributedItemContainer}.
     * @param imageUrl The URL of the image to be loaded.
     * @return A Task that loads the image from the specified URL and updates the imageView upon completion.
     */
    private Task<Image> loadImageAsync(String imageUrl) {
        loadImageTask = new Task<Image>() {
            @Override
            protected Image call() throws Exception {
                return new Image(imageUrl, true);
            }
        };
        
        loadImageTask.setOnSucceeded(e -> {
            imageView.setImage(loadImageTask.getValue());
        });
        
        Thread imageThread = new Thread(loadImageTask);
        imageThread.setDaemon(true);//Allow JVM to exit if these threads are the only ones left
        imageThread.start();
        return loadImageTask;
    }
}