/*
* This file is part of the Trickl Open Source Libraries.
*
* Trickl Open Source Libraries - http://open.trickl.com/
*
* Copyright (C) 2011 Tim Gee.
*
* Trickl Open Source Libraries are 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 3 of the License, or
* (at your option) any later version.
*
* Trickl Open Source Libraries are 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 project. If not, see <http://www.gnu.org/licenses/>.
*/
package com.trickl.graph.planar.generate;
import com.trickl.graph.planar.PlanarGraph;
import com.trickl.graph.planar.PlanarGraphs;
import com.trickl.graph.planar.PlanarLayout;
import com.vividsolutions.jts.algorithm.Angle;
import com.vividsolutions.jts.geom.Coordinate;
import java.util.*;
import org.jgrapht.VertexFactory;
public class PlanarCircleGraphGenerator<V, E>
implements PlanarGraphGenerator<V, E, V>, PlanarLayout<V> {
private static class Site implements Comparable<Site>, PlanarLayout<Coordinate> {
// Logical Co-ords
final private static double tolerance = 1e-6;
final private double scale;
final private Coordinate logicalPosition;
final private int discoveryDirection;
final private AngleComparator<Coordinate> angleComparator;
public Site(Coordinate logicalPosition, double scale) {
this(logicalPosition, scale, 0);
}
public Site(Coordinate logicalPosition, double scale, int discoveryDirection) {
this.logicalPosition = logicalPosition;
this.discoveryDirection = discoveryDirection;
this.scale = scale;
angleComparator = new AngleComparator<Coordinate>(new Coordinate(1, 0), new Coordinate(0, 0), this);
}
public Coordinate getLogicalPosition() {
return logicalPosition;
}
@Override
public Coordinate getCoordinate(Coordinate coord) {
return coord;
}
public Coordinate getCoordinate() {
return new Coordinate(Math.sqrt(3) * scale * logicalPosition.x, scale * logicalPosition.y);
}
public double distanceToOrigin() {
return getCoordinate().distance(new Coordinate(0, 0));
}
private int getDiscoveryDirection() {
return discoveryDirection;
}
/**
* Natural order of sites is those closest to the origin and
* in the case of a tie, clockwise.
* @param other
* @return
*/
@Override
public int compareTo(Site other) {
if (Math.abs(distanceToOrigin() - other.distanceToOrigin()) < scale * tolerance) {
return -angleComparator.compare(this.getCoordinate(), other.getCoordinate());
} else if (distanceToOrigin() > other.distanceToOrigin()) {
return 1;
} else {
return -1;
}
}
@Override
public boolean equals(Object other) {
if (other instanceof Site
&& ((Site) other).logicalPosition.equals(logicalPosition))
return true;
return false;
}
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + (this.logicalPosition != null ? this.logicalPosition.hashCode() : 0);
return hash;
}
}
private static class AngleComparator<V> implements Comparator<V> {
final private V source;
final private V pivot;
final private PlanarLayout<V> planarLayout;
AngleComparator(V source, V pivot, PlanarLayout<V> planarLayout) {
this.source = source;
this.pivot = pivot;
this.planarLayout = planarLayout;
}
@Override
public int compare(V first, V second) {
// Compare angle with respect to the common vertex (the pivot)
return -Double.compare(getOrientedAngle(source, pivot, first),
getOrientedAngle(source, pivot, second));
}
private double getOrientedAngle(V source, V pivot, V target) {
if (source.equals(target)) return 0;
Coordinate sourcePosition = planarLayout.getCoordinate(source);
Coordinate pivotPosition = planarLayout.getCoordinate(pivot);
Coordinate targetPosition = planarLayout.getCoordinate(target);
double smallerAngle = Angle.angleBetween(sourcePosition, pivotPosition, targetPosition);
int turn = Angle.getTurn(Angle.angle(pivotPosition, sourcePosition),
Angle.angle(pivotPosition, targetPosition));
double angle = 0;
switch(turn) {
case Angle.COUNTERCLOCKWISE:
angle = smallerAngle;
break;
case Angle.CLOCKWISE:
angle = -smallerAngle;
break;
case Angle.NONE:
default:
break;
}
return angle;
}
}
final private int size;
final private double scale;
final private Map<V, Site> sites = new HashMap<V, Site>();
public PlanarCircleGraphGenerator(int size) {
this(size, 1);
}
public PlanarCircleGraphGenerator(int size, double scale) {
this.size = size;
this.scale = scale;
}
@Override
public Coordinate getCoordinate(V vertex) {
return sites.get(vertex).getCoordinate();
}
@Override
public void generateGraph(PlanarGraph<V, E> graph, VertexFactory<V> vertexFactory,
java.util.Map<java.lang.String, V> resultMap) {
ArrayList<V> vertices = new ArrayList<V>(size);
for (int k = 0; k < size; ++k) {
V vertex = vertexFactory.createVertex();
vertices.add(vertex);
graph.addVertex(vertex);
}
Map<Coordinate, Integer> existingSites = new HashMap<Coordinate, Integer>();
PriorityQueue<Site> potentialSitesQueue = new PriorityQueue<Site>();
potentialSitesQueue.add(new Site(new Coordinate(0, 0), scale));
// Find the nearest potential vertex to the centre
for (int k = 0; k < size; ++k) {
Site site = potentialSitesQueue.poll();
V vertex = vertices.get(k);
Coordinate position = site.getLogicalPosition();
sites.put(vertices.get(k), site);
existingSites.put(position, k);
// Add new potential sites as neighbours of this one
for (int orientationIndex = 0; orientationIndex < 6; ++orientationIndex) {
// Base the neighbour on a logical hexagonal grid
Coordinate neighbourPosition = new Coordinate(position.x, position.y);
// Search for neighbours starting by looking outwards from the grid
// (outwards can be determined by the site discovery orientation)
int direction = (orientationIndex + site.getDiscoveryDirection()) % 6;
switch (direction) {
case 0:
neighbourPosition.y += 2;
break;
case 1:
neighbourPosition.x -= 1;
neighbourPosition.y += 1;
break;
case 2:
neighbourPosition.x -= 1;
neighbourPosition.y -= 1;
break;
case 3:
neighbourPosition.y -= 2;
break;
case 4:
neighbourPosition.x += 1;
neighbourPosition.y -= 1;
break;
case 5:
neighbourPosition.x += 1;
neighbourPosition.y += 1;
break;
}
Integer k_neighbour = existingSites.get(neighbourPosition);
if (k_neighbour != null) {
V neighbour = vertices.get(k_neighbour);
addEdge(graph, neighbour, vertex);
addEdge(graph, vertex, neighbour);
} else {
Site neighbourSite = new Site(neighbourPosition, scale, direction);
if (!potentialSitesQueue.contains(neighbourSite)) {
potentialSitesQueue.add(neighbourSite);
}
}
}
}
}
private E addEdge(PlanarGraph<V, E> graph, V source, V target) {
if (graph.containsEdge(source, target)) return
graph.getEdge(source, target);
AngleComparator<V> angleComparator = new AngleComparator<V>(target, source, this);
List<V> perimeter = PlanarGraphs.getConnectedVertices(graph, source);
V before = null;
if (!perimeter.isEmpty())
{
Collections.sort(perimeter, angleComparator);
int index = Collections.binarySearch(perimeter, target, angleComparator);
if (index < 0) {
// Find the first edge clockwise from this edge
before = perimeter.get((-index - 2 + perimeter.size()) % perimeter.size());
}
else {
before = perimeter.get(index);
}
}
return graph.addEdge(source, target, before, null);
}
}