de.nec.nle.siafu.graphics.Painter.java Source code

Java tutorial

Introduction

Here is the source code for de.nec.nle.siafu.graphics.Painter.java

Source

/*
 * Copyright NEC Europe Ltd. 2006-2007
 * 
 * This file is part of the context simulator called Siafu.
 * 
 * Siafu is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 
 * Siafu is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package de.nec.nle.siafu.graphics;

import java.io.InputStream;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TreeSet;

import org.apache.commons.lang.NotImplementedException;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;

import de.nec.nle.siafu.control.Controller;
import de.nec.nle.siafu.graphics.Markers.Type;
import de.nec.nle.siafu.graphics.markers.Marker;
import de.nec.nle.siafu.model.Agent;
import de.nec.nle.siafu.model.Overlay;
import de.nec.nle.siafu.model.Place;
import de.nec.nle.siafu.model.Position;
import de.nec.nle.siafu.model.Trackable;
import de.nec.nle.siafu.model.World;

/**
 * This class handles the actual drawing on the simulation's canvas for most of
 * the simulator's graphics. A notable exception are Markers, which draw
 * themselves.
 * 
 * @author Miquel Martin
 * 
 */
public class Painter {
    /** Mask for the places overlay palette. */
    private static final int PLACE_PALETTE_MASK = 0x3FC;

    /**
     * How many minutes per hour. On earth, of course. Or any similar planet
     * with equal rotation and translation period. Yes, checkstyle forced me to
     * remove the magic number.
     */
    private static final float MINUTES_PER_HOUR = 60f;

    /** How much to darken the background. */
    private static final int DARKNESS_STEP = 100;

    /** Blue mask. */
    private static final int BLUE_MASK = 0x0000FF;

    /** Gren mask. */
    private static final int GREEN_MASK = 0x00FF00;

    /** Red mask. */
    private static final int RED_MASK = 0xFF0000;

    /**
     * When the blue sky is painted on the background, this value defines how
     * transparent the blue should be.
     */
    private static final int BG_ALPHA_STEP = 0xA0;

    /** The maximum direction value. */
    private static final int MAX_DIR = 8;

    /** Er... 0xFF. Checkstyle forced me. */
    private static final int FF = 0xff;

    /** Name says it all. Checkstyle forced me. */
    private static final int ONE_HUNDRED = 100;

    /** Color depth of the images. */
    private static final int COLOR_DEPTH = 24;

    /** Alpha value when drawing an overlay on the background. */
    private static final int OVERLAY_ALPHA = 170;

    /** The SWT display. */
    private Display display;

    /**
     * Sunrise hour.
     */
    private static final int SUNRISE = 6;

    /**
     * Sunset hour.
     */
    private static final int SUNSET = 20;

    /**
     * How many steps does it take to go from full light to full darkness.
     */
    private static final int SUNSET_STEPS = 5;

    /** The image of the background with an overlay on top. */
    private Image overlayImg;

    /** The sprites of the agents, sorted by sprite name. */
    private HashMap<String, Sprite> personImg = new HashMap<String, Sprite>();

    /** A reference to the Siafu controller. */
    private Controller control;

    /** A reference to the simulation's world. */
    private World world;

    /** The ImageData for the background image. */
    private ImageData backgroundData = null;

    /** The ImageData for a blue image (used for the sky). */
    private ImageData skyData = null;

    /**
     * The set of images that make the background, where 0 is normal, and the
     * rest show a progressive increase the sky darnkess.
     */
    private Image[] background = null;

    /** The simulation time. */
    private Calendar time;

    /**
     * Instantiate a painter.
     * 
     * @param control
     *            the Controller object for Siafu
     */
    public Painter(final Controller control) {
        this.control = control;
        this.world = control.getWorld();
        this.time = world.getTime();
        this.display = Display.getDefault();
        preloadPeopleImages();

        backgroundData = new ImageData(control.getSimulationData().getBackgroundFile());

        prepareBackgrounds();
    }

    /**
     * Create a set of darkened background for the night time of the simulation.
     */
    private void prepareBackgrounds() {
        prepareSky();

        background = new Image[SUNSET_STEPS];
        Controller.getProgress().reportBackgroundCreationStart(SUNSET_STEPS);

        for (int i = 0; i < SUNSET_STEPS; i++) {
            Controller.getProgress().reportBackgroundCreated();
            background[i] = getDarkenedBackground(ONE_HUNDRED * i / SUNSET_STEPS);
        }
        Controller.getProgress().reportBackgroundCreationEnd();
    }

    /**
     * Create a blue image data that will be used as the sky color.
     */
    private void prepareSky() {
        int width = backgroundData.width;
        int height = backgroundData.height;
        RGB[] colors = new RGB[] { new RGB(0, 0, FF), new RGB(0, 0, FF) };
        PaletteData palette = new PaletteData(colors);
        palette.isDirect = false;
        skyData = new ImageData(width, height, 1, palette);

        int[] blueRow = new int[width];
        for (int i = 0; i < blueRow.length; i++) {
            blueRow[i] = 1;
        }
        for (int i = 0; i < height; i++) {
            skyData.setPixels(0, i, width, blueRow, 0);
        }
    }

    /**
     * Load the images for all the sprites that will be used in the simulation.
     */
    private void preloadPeopleImages() {
        HashMap<String, InputStream> rawSprites = control.getSimulationData().getSprites();
        for (String fileName : rawSprites.keySet()) {
            String spriteName = fileName.split("-")[0];

            // Have we processed another direction of this sprite?
            if (personImg.containsKey(spriteName)) {
                continue;
            }
            Sprite s;
            try {
                String[] offset = fileName.split("-")[1].split("\\.");
                s = new Sprite(spriteName, new Integer(offset[0]), new Integer(offset[1]));

                for (int dir = 0; dir < MAX_DIR; dir++) {
                    s.setImage(dir, new Image(display,
                            rawSprites.get(spriteName + "-" + s.getVOffset() + "." + s.getHOffset() + "-" + dir)));
                }
            } catch (Exception e) {
                throw new RuntimeException("Can't read the sprites for " + spriteName, e);
            }

            personImg.put(s.getName(), s);
        }
    }

    /**
     * Generate an image of the original background, only darkened a certain
     * percentage. 100 will render a completely dark blue background.
     * 
     * @param percent
     *            the percentage of dark blue to add to the background
     * @return the darkened Image
     */
    private Image getDarkenedBackground(final int percent) {
        byte alfa;
        Image img;
        if (percent < 0) {
            alfa = 0;
        } else if (percent > ONE_HUNDRED) {
            alfa = ONE_HUNDRED;
        } else {
            alfa = (byte) (BG_ALPHA_STEP * (percent) / (double) ONE_HUNDRED);
        }

        if (!backgroundData.palette.isDirect) {
            backgroundData = makePaletteDirect(backgroundData);
        }
        img = new Image(Display.getCurrent(), backgroundData);

        skyData.alpha = alfa;
        GC imgGC = new GC(img);
        imgGC.drawImage(new Image(Display.getCurrent(), skyData), 0, 0);
        imgGC.dispose();

        return img;
    }

    /**
     * Turn the background image into an image with a direct palette. By the
     * way, PNG's can have indexed palettes, and Windows will keep that when you
     * draw on it. Linux, on the other hand, will treat any PNG as direct
     * palette (non=indexed). Since we are drawing overlays on top of the
     * original image, we definitelly need more colors than the original image
     * has.
     * 
     * @param originalData
     *            the original picture, potentially with a direct palette
     * @return a new ImageData where the pixel values correspond to RGB values,
     *         instead of the original, indexed one. If the original palette was
     *         already direct, the ImageData you get back is still a copy of the
     *         original
     */
    public ImageData makePaletteDirect(final ImageData originalData) {
        // Make a direct palette image
        PaletteData pd = new PaletteData(RED_MASK, GREEN_MASK, BLUE_MASK);
        ImageData baseData = new ImageData(originalData.width, originalData.height, COLOR_DEPTH, pd);
        Image base = new Image(Display.getCurrent(), baseData);

        // Draw our image on top to convert indexed pixel values into RGB
        // values
        GC baseGC = new GC(base);
        Image originalImg = new Image(Display.getCurrent(), originalData);
        baseGC.drawImage(originalImg, 0, 0);
        baseGC.dispose();

        // Get our fresh new direct palette imagedata
        ImageData converted = base.getImageData();

        // Clean up and return
        originalImg.dispose();
        base.dispose();

        return converted;
    }

    /**
     * Choose how dark the background should be depending on the time of the
     * day.
     * 
     * @return the index of the required background
     */
    private int chooseDarknessLevel() {
        int hour = time.get(Calendar.HOUR_OF_DAY);
        int minute;

        if ((hour > SUNRISE) && (hour < SUNSET)) {
            return 0; // Day
        } else if ((hour < SUNRISE) || (hour > SUNSET)) {
            return ONE_HUNDRED; // Night
        } else {
            minute = time.get(Calendar.MINUTE);
            int darkness = (int) (DARKNESS_STEP * (minute / MINUTES_PER_HOUR));
            if (hour == SUNRISE) {
                return ONE_HUNDRED - darkness;
            } else {
                return darkness;
            }
        }
    }

    /**
     * Draw the background image (darkened it it's late at night) or the
     * background plus an overlay if the user is requesting it.
     * 
     * @param gc
     *            the GC on which to draw
     */
    public synchronized void paintBackground(final GC gc) {
        if (overlayImg == null) {
            int bgIndex = 0;
            if (control.getGUI().isNightSimulated()) {
                double darkness = chooseDarknessLevel() / (double) ONE_HUNDRED;
                bgIndex = (int) ((SUNSET_STEPS - 1) * (darkness));
            }
            gc.drawImage(background[bgIndex], 0, 0);
        } else {
            gc.drawImage(overlayImg, 0, 0);
        }
    }

    /**
     * Get the sprite for a particular agent. This is derived from the agent's
     * image.
     * 
     * @param a
     *            the agent whose sprite we need
     * @return the agent's sprite
     */
    public Sprite getAgentSprite(final Agent a) {
        return personImg.get(a.getImage());
    }

    /**
     * Paint the agents in the simulation.
     * 
     * @param gc
     *            the GC on which to draw
     */
    public void paintPeople(final GC gc) {
        // Put into a treeset to guarantee the order for the z layer
        Iterator<Agent> it = new TreeSet<Agent>(world.getPeople()).iterator();

        if (control.getGUI().isPathShown()) {
            paintPath(gc);
        }

        while (it.hasNext()) {
            Agent a = it.next();

            if (a.isVisible()) {

                Sprite s = personImg.get(a.getImage());

                if (s == null) {
                    throw new RuntimeException("Unknown sprite " + a.getImage());
                }
                gc.drawImage(s.getImage(a.getDir()), a.getPos().getCol() - (s.getHOffset()),
                        a.getPos().getRow() - s.getVOffset());
            }
        }
    }

    /**
     * Draw the path that an agent is following in orer to reach its
     * destination.
     * 
     * @param gc
     *            the GC on which to draw
     * 
     */
    public void paintPath(final GC gc) {
        Trackable t = control.getGUI().getActive();
        if (t instanceof Agent) {
            Agent a = (Agent) t;
            if (a.getDestination() != null) {
                gc.setForeground(display.getSystemColor(SWT.COLOR_RED));
                // We use a temporary agent that "walks the walk"
                Agent path = new Agent(a.getPos(), "HumanBlue", world);
                path.setDir(a.getDir());
                path.setSpeed(2);
                path.setDestination(a.getDestination());
                while (!path.isAtDestination()) {
                    Position pos = path.getPos();
                    gc.drawPoint(pos.getCol(), pos.getRow());
                    path.moveTowardsDestination();
                }
            }
        }
    }

    /**
     * Get the bounds of the background.
     * 
     * @return a Rectangle with the bounds.
     */
    public Rectangle getBounds() {
        return new Rectangle(0, 0, backgroundData.width, backgroundData.height);
    }

    /**
     * Ask all the markers to draw themselves with the given GC.
     * 
     * @param gc
     *            the GC on which to draw
     * @param markers
     *            the Markers object with all of the simulation's markers
     */
    public void paintMarkers(final GC gc, final Markers markers) {
        for (Type t : Type.values()) {
            for (Marker m : t.getMarkers()) {
                m.draw(gc);
            }
        }
    }

    /**
     * Called by the controller, this method draws the overlay on top of the
     * background, and keep the image ready to be painted on the canvas instead
     * of the background.
     * 
     * @param ov
     *            the overlay that needs drawing
     */
    public synchronized void requestOverlayDrawing(final Overlayable ov) {
        overlayImg = createOverlayImg(ov);
    }

    /**
     * Destroy the overlay+background image. The painter will now resume drawing
     * the background.
     * 
     */
    public synchronized void cancelOverlayDrawing() {
        overlayImg.dispose();
        overlayImg = null;

    }

    /**
     * Create an overlay image by drawing the overlay itself on the background,
     * with a semi transparent alpha value.
     * 
     * @param ov
     *            the overlay to be drawn
     * @return the overlay+background image
     */
    private Image createOverlayImg(final Overlayable ov) {
        Image img;

        /*
         * In the case of places, We choose a palette that only uses some of the
         * lower bits (0x3FC) of the distance integer. This creates a cyclical
         * gradient, which is easier to follow than a very smooth and large one.
         * By setting the mask for R, G and B to the same value, we obtain gray
         * overlays.
         */
        PaletteData palette;
        int[][] values;

        if (ov instanceof Place) {
            palette = new PaletteData(PLACE_PALETTE_MASK, PLACE_PALETTE_MASK, PLACE_PALETTE_MASK);
            values = ((Place) ov).getGradient().getDistances();
        } else if (ov instanceof Overlay) {
            palette = new PaletteData(RED_MASK, GREEN_MASK, BLUE_MASK);
            values = ((Overlay) ov).getValueMatrix();
        } else {
            throw new NotImplementedException("Can't draw overlayables of this type");
        }

        ImageData ovLayer = new ImageData(backgroundData.width, backgroundData.height, COLOR_DEPTH, palette);

        for (int i = 0; i < values.length; i++) {
            ovLayer.setPixels(0, i, values[0].length, values[i], 0);
        }
        Image ovLayerImg = new Image(display, ovLayer);

        img = new Image(display, backgroundData);
        GC imgGC = new GC(img);
        imgGC.setAlpha(OVERLAY_ALPHA);
        imgGC.drawImage(ovLayerImg, 0, 0);
        return img;

    }

    /**
     * Destroy all SWT allocated resources.
     * 
     */
    public void disposeResources() {
        if (overlayImg != null) {
            overlayImg.dispose();
        }
        for (Sprite sprite : personImg.values()) {
            sprite.disposeResources();
        }
        for (Image img : background) {
            img.dispose();
        }
    }
}