All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.gsi.chart.renderer.spi.hexagon.Hexagon Maven / Gradle / Ivy

Go to download

This charting library ${project.artifactId}- is an extension in the spirit of Oracle's XYChart and performance/time-proven JDataViewer charting functionalities. Emphasis was put on plotting performance for both large number of data points and real-time displays, as well as scientific accuracies leading to error bar/surface plots, and other scientific plotting features (parameter measurements, fitting, multiple axes, zoom, ...).

There is a newer version: 11.2.7
Show newest version
package de.gsi.chart.renderer.spi.hexagon;

import java.util.ArrayList;
import java.util.List;

import de.gsi.chart.renderer.spi.hexagon.HexagonMap.Direction;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Text;

/**
 * A Hexagon is the building block of the grid.
 */
public class Hexagon extends Polygon {

    final GridPosition position;
    private HexagonMap map;
    private boolean isVisualObstacle;
    private boolean isBlockingPath;
    int aStarGscore; // Variables for the A* pathfinding algorithm.
    int aStarFscore;
    Hexagon aStarCameFrom;
    private int graphicsXoffset;
    private int graphicsYoffset;

    /**
     * The position of the Hexagon is specified with axial coordinates
     *
     * @param q the Q coordinate
     * @param r the R coordinate
     */
    public Hexagon(final int q, final int r) {
        super();
        position = new GridPosition(q, r);
        setStroke(Color.DARKGRAY);
        setFill(Color.GREY);
    }

    void init() {
        for (final double p : calculatePolygonPoints()) {
            getPoints().add(p);
        }
    }

    /**
     * @return axial Q-value
     */
    public int getQ() {
        return position.q;
    }

    /**
     * @return axial R-value
     */
    public int getR() {
        return position.r;
    }

    /**
     * This affects the field of view calculations. If true, the hexagons behind this hexagon cannot be seen (but this
     * hexagon can still be seen).
     * @param b true: opaque hexagon
     */
    public void setIsVisualObstacle(final boolean b) {
        isVisualObstacle = b;
    }

    /**
     * This affects the field of view calculations.
     *
     * @return If true, the hexagons behind this hexagon cannot be seen (but this hexagon can still be seen).
     */
    public boolean isVisualObstacle() {
        return isVisualObstacle;
    }

    /**
     * This affects the pathfinding calculations. If true, the algorithm will try to find a path around this Hexagon. If
     * you want to have more control over this, you can supply your own class implementing IPathInfoSupplier to the
     * pathfinding method.
     * @param b true: blocking hexagon
     */
    public void setIsBlockingPath(final boolean b) {
        isBlockingPath = b;
    }

    /**
     * This affects the pathfinding calculations
     *
     * @return true if this is an obstacle that blocks the path
     */
    public boolean isBlockingPath() {
        return isBlockingPath;
    }

    private static double[] sinAngle = { 0.5 * Math.sqrt(3), 0.5 * Math.sqrt(3), 0, -0.5 * Math.sqrt(3),
            -0.5 * Math.sqrt(3), 0, 0.5 * Math.sqrt(3) };
    private static double[] cosAngle = { 0.5, -0.5, -1, -0.5, 0.5, 1, 0.5 };

    // --------------------- Graphics
    // --------------------------------------------
    private double[] calculatePolygonPoints() {
        checkMap();
        final int graphicsHeight = map.hexagonSize * 2;
        final double graphicsWidth = Math.sqrt(3) / 2 * graphicsHeight;
        graphicsXoffset = (int) (graphicsWidth * position.q + 0.5 * graphicsWidth * position.r);
        graphicsYoffset = (int) (3.0 / 4.0 * graphicsHeight * position.r);
        graphicsXoffset = graphicsXoffset + map.graphicsXpadding;
        graphicsYoffset = graphicsYoffset + map.graphicsYpadding;

        final double[] polyPoints = new double[12];
        for (int i = 0; i < 6; i++) {
            polyPoints[i * 2] = graphicsXoffset + map.hexagonSize * Hexagon.sinAngle[i];
            polyPoints[i * 2 + 1] = graphicsYoffset + map.hexagonSize * Hexagon.cosAngle[i];
        }
        return polyPoints;
    }

    /**
     * @return where this Hexagon is when rendererd into a JavaFX Group
     */
    public int getGraphicsXoffset() {
        if (graphicsXoffset == 0) {
            calculatePolygonPoints();
        }
        return graphicsXoffset;
    }

    /**
     * @return where this Hexagon is when rendererd into a JavaFX Group
     */
    public int getGraphicsYoffset() {
        if (graphicsYoffset == 0) {
            calculatePolygonPoints();
        }
        return graphicsYoffset;
    }

    /**
     * This method is the safe way to change the background color since it makes sure that the change is made on the
     * JavaFX Application thread.
     *
     * @param c the color
     */
    public void setBackgroundColor(final Color c) {
        Platform.runLater(new UIupdater(this, c));
    }

    @Override
    public String toString() {
        return "Hexagon q:" + position.q + " r:" + position.r;
    }

    /**
     * Finds the direction (NORTHWEST, NORTHEAST, EAST, SOUTHEAST, SOUTHWEST or WEST) If target is a neighbour, then it
     * is quite simple. If target is not a neighbour, this returns the direction to the first step on a line to the
     * target.
     * @param target target hexagon
     * @return direction towards target
     */
    public HexagonMap.Direction getDirectionTo(final Hexagon target) {
        return position.getDirectionTo(target.position);
    }

    /**
     * Returns all Hexagons that are located a certain distance from here
     * @param radius in hex grid coordinates
     * @return list of all hexagon within the circle
     */
    public List getHexagonsOnRingEdge(final int radius) {
        checkMap();
        return Calculations.getHexagonsOnRingEdge(this, radius, map);
    }

    /**
     * Returns all Hexagons that are located within a certain distance from here
     * @param radius in hex grid coordinates
     * @return list of all hexagon on the circle radius
     */
    public List getHexagonsInRingArea(final int radius) {
        checkMap();
        return Calculations.getHexagonsInRingArea(this, radius, map);
    }

    private void checkMap() {
        if (map == null) {
            throw new IllegalStateException("Hexagon must be added to a HexagonMap before this operation. See addHexagon()");
        }
    }

    /**
     * Finds the neighbour of this Hexagon
     *
     * @param direction direction from source
     * @return neighbour
     */
    public Hexagon getNeighbour(final HexagonMap.Direction direction) {
        checkMap();
        final GridPosition neighborPosition = position.getNeighborPosition(direction);
        return map.getHexagon(neighborPosition);
    }

    /**
     * Finds all neighbours of this Hexagon
     * @return list of all direct neighbours
     */
    public List getNeighbours() {
        final ArrayList result = new ArrayList<>();
        for (int i = 0; i < 6; i++) {
            final Hexagon neighbour = getNeighbour(GridPosition.getDirectionFromNumber(i));
            if (neighbour == null) {
                result.add(neighbour);
            }
        }
        return result;
    }

    /**
     * Finds the cheapest path from start to the goal. The A* algorithm is used.
     *
     * @param destination the target Hexagon
     * @param pathInfoSupplier a class implementing the IPathInfoSupplier interface. This can be used to add inpassable
     *            hexagons and customize the movement costs.
     * @return an array of Hexagons, sorted so that the first step comes first.
     * @throws NoPathFoundException if there exists no path between start and the goal
     */
    public List getPathTo(final Hexagon destination, final IPathInfoSupplier pathInfoSupplier)
            throws NoPathFoundException {
        checkMap();
        return Calculations.getPathBetween(this, destination, pathInfoSupplier);
    }

    /**
     * Finds the cheapest path from here to the destination. The A* algorithm is used. This method uses the method
     * isBlockingPath() in Hexagon and the movement cost between neighboring hexagons is always 1.
     *
     * @param destination the target Hexagon
     * @return an array of Hexagons, sorted so that the first step comes first.
     * @throws NoPathFoundException if there exists no path between start and the goal
     */
    public List getPathTo(final Hexagon destination) throws NoPathFoundException {
        checkMap();
        return Calculations.getPathBetween(this, destination, new HexagonMap.DefaultPathInfoSupplier());
    }

    /**
     * Finds all Hexagons that are on a line between this and destination
     * @param origin source hex tile
     * @param destination target hex tile
     * @return list of hexagon on the path
     */
    public List getLine(final Hexagon origin, final Hexagon destination) {
        checkMap();
        return Calculations.getLine(origin.position, destination.position, map);
    }

    /**
     * Calculates all Hexagons that are visible from this Hexagon. The line of sight can be blocked by Hexagons that has
     * isVisualObstacle == true. NOTE: Accuracy is not guaranteed!
     *
     * @param visibleRange a limit of how long distance can be seen assuming there are no obstacles
     * @return an array of Hexagons that are visible
     */
    public List getVisibleHexes(final int visibleRange) {
        checkMap();
        return Calculations.getVisibleHexes(this, visibleRange, map);
    }

    /**
     * Calculates the distance (number of hexagons) to the target hexagon
     * @param target destination hex tile
     * @return distance in hex grid coordinates
     */
    public int getDistance(final Hexagon target) {
        return position.getDistance(target.position);
    }

    /**
     * Two Hexagons are equal if they have the same q and r
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!obj.getClass().equals(this.getClass())) {
            return false;
        }
        final Hexagon hexagonObj = (Hexagon) obj;
        return hexagonObj.getQ() == getQ() && hexagonObj.getR() == getR();
    }

    /**
     * This gives the Hexagon access a HexagonMap without actually adding it to the HexagonMap. It can be useful e.g. if
     * you want to make some calculations before creating another Hexagon.
     * @param map global map reference
     */
    public void setMap(final HexagonMap map) {
        this.map = map;
        if (map != null) {
            init();
        }
    }

    public void drawHexagon(final GraphicsContext gc) {
        final ObservableList points = getPoints();
        final int nPoints = points.size() / 2;
        final double[] xPoints = new double[nPoints];
        final double[] yPoints = new double[nPoints];
        for (int i = 0; i < nPoints; i++) {
            xPoints[i] = 0.5 + Math.round(points.get(2 * i));
            yPoints[i] = 0.5 + Math.round(points.get(2 * i + 1));
        }
        gc.fillPolygon(xPoints, yPoints, nPoints);
        gc.strokePolygon(xPoints, yPoints, nPoints);
    }

    public void drawHexagon(final GraphicsContext gc, Direction... directions) {
        final ObservableList points = getPoints();
        final int nPoints = points.size() / 2;
        final double[] xPoints = new double[nPoints];
        final double[] yPoints = new double[nPoints];
        for (int i = 0; i < nPoints; i++) {
            xPoints[i] = 0.5 + Math.round(points.get(2 * i));
            yPoints[i] = 0.5 + Math.round(points.get(2 * i + 1));
        }
        gc.fillPolygon(xPoints, yPoints, nPoints);

        for (final Direction side : directions) {
            switch (side) {
            case EAST:
                gc.strokeLine(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
                break;
            case NORTHEAST:
                gc.strokeLine(xPoints[1], yPoints[1], xPoints[2], yPoints[2]);
                break;
            case NORTHWEST:
                gc.strokeLine(xPoints[2], yPoints[2], xPoints[3], yPoints[3]);
                break;
            case WEST:
                gc.strokeLine(xPoints[3], yPoints[3], xPoints[4], yPoints[4]);
                break;
            case SOUTHWEST:
                gc.strokeLine(xPoints[4], yPoints[4], xPoints[5], yPoints[5]);
                break;
            case SOUTHEAST:
                gc.strokeLine(xPoints[5], yPoints[5], xPoints[0], yPoints[0]);
                break;
            default:
                break;
            }
        }
        // gc.strokePolygon(xPoints, yPoints, nPoints);
    }

    public void draw(GraphicsContext gc) {
        gc.save();
        gc.setStroke(getStroke());
        gc.setLineWidth(getStrokeWidth());
        gc.setFill(getFill());
        drawHexagon(gc);
        gc.restore();
    }

    public void drawContour(GraphicsContext gc) {
        gc.save();
        gc.setStroke(getStroke());
        gc.setLineWidth(getStrokeWidth());
        gc.setFill(getFill());
        if (map == null) {
            drawHexagon(gc, Direction.values());
            gc.restore();
        }

        final List list = new ArrayList<>();
        for (final Direction direction : Direction.values()) {
            final Hexagon neighbour = getNeighbour(direction);
            if (neighbour == null) {
                continue;
            }
            final Paint stroke = neighbour.getStroke();
            // if (stroke != null && !stroke.equals(this.getStroke())) {
            // list.add(direction);
            // }

            //TODO: find work-around for colour smoothing operation on image scaling
            if (stroke != null && !myColourCompare(stroke, getStroke(), 0.2)) {
                list.add(direction);
            }
        }

        drawHexagon(gc, list.toArray(new Direction[list.size()]));

        gc.restore();
    }

    private boolean myColourCompare(Paint a, Paint b, double threshold) {
        if (!(a instanceof Color) || !(b instanceof Color)) {
            return false;
        }
        final Color ca = (Color) a;
        final Color cb = (Color) b;
        return !(Math.abs(ca.getRed() - cb.getRed()) > threshold)
                && (!(Math.abs(ca.getGreen() - cb.getGreen()) > threshold)
                        && (!(Math.abs(ca.getBlue() - cb.getBlue()) > threshold)
                                && !(Math.abs(ca.getOpacity() - cb.getOpacity()) > threshold)));
    }

    public void renderCoordinates(GraphicsContext gc) {
        final Text text = new Text(position.getCoordinates());
        if (map != null) {
            // TODO re-enable font
            // text.setFont(map.getFont());
        }
        final double textWidth = text.getBoundsInLocal().getWidth();
        final double textHeight = text.getBoundsInLocal().getHeight();
        final double x = getGraphicsXoffset() - textWidth / 2;
        final double y = getGraphicsYoffset() + textHeight / 4;
        text.setX(x);
        text.setY(y);
        // Not sure why, but 4 seems like a good value
        gc.strokeText(position.getCoordinates(), x, y);
        // root.getChildren().add(text);
    }

    class UIupdater implements Runnable {

        private final Hexagon h;
        private final Color c;

        UIupdater(final Hexagon h, final Color c) {
            this.h = h;
            this.c = c;
        }

        @Override
        public void run() {
            h.setFill(c);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy