Skip to content

Commit

Permalink
first try
Browse files Browse the repository at this point in the history
  • Loading branch information
i-make-robots committed Oct 14, 2024
1 parent ed1cd17 commit ef15130
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 8 deletions.
16 changes: 8 additions & 8 deletions src/main/java/com/marginallyclever/makelangelo/MainMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -366,36 +366,36 @@ private JMenu createToolsMenu() {

menu.addSeparator();

TurtleTool a4 = new FlipTurtleAction(1,-1,Translator.get("FlipV"));
var a4 = new FlipTurtleAction(1,-1,Translator.get("FlipV"));
a4.putValue(Action.SMALL_ICON, new ImageIcon(Objects.requireNonNull(getClass().getResource("/com/marginallyclever/makelangelo/icons8-flip-horizontal-16.png"))));
a4.setSource(app);
a4.addModifierListener(app::setTurtle);
menu.add(a4);

TurtleTool a5 = new FlipTurtleAction(-1,1,Translator.get("FlipH"));
var a5 = new FlipTurtleAction(-1,1,Translator.get("FlipH"));
a5.putValue(Action.SMALL_ICON, new ImageIcon(Objects.requireNonNull(getClass().getResource("/com/marginallyclever/makelangelo/icons8-flip-vertical-16.png"))));
a5.setSource(app);
a5.addModifierListener(app::setTurtle);
menu.add(a5);

menu.addSeparator();

TurtleTool a1 = createModifier(new SimplifyTurtleAction(),null);
var a1 = createModifier(new SimplifyTurtleAction(),null);
a1.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_Y, SHORTCUT_CTRL));//"ctrl Y"
//a1.putValue(Action.SMALL_ICON,new ImageIcon(Objects.requireNonNull(getClass().getResource("/com/marginallyclever/makelangelo/icons8-simplify-16.png"))));
menu.add(a1);

TurtleTool a2 = createModifier(new ReorderTurtleAction(),"/com/marginallyclever/makelangelo/icons8-sort-16.png");
var a2 = createModifier(new ReorderTurtleAction(),"/com/marginallyclever/makelangelo/icons8-sort-16.png");
a2.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_R, SHORTCUT_CTRL));//"ctrl R"
menu.add(a2);

TurtleTool a3 = createModifier(new InfillTurtleAction(), "/com/marginallyclever/makelangelo/icons8-fill-color-16.png");
var a3 = createModifier(new InfillTurtleAction(), "/com/marginallyclever/makelangelo/icons8-fill-color-16.png");
a3.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_I, SHORTCUT_CTRL));//"ctrl I"
menu.add(a3);

LeastHopsTurtleAction a4 = createModifier(new LeastHopsTurtleAction(),"/com/marginallyclever/makelangelo/icons8-kangaroo-16.png");
a4.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_H, SHORTCUT_CTRL));//"ctrl H"
menu.add(a4);
var a6 = createModifier(new LeastHops(),"/com/marginallyclever/makelangelo/icons8-kangaroo-16.png");
a6.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_H, SHORTCUT_CTRL));//"ctrl H"
menu.add(a6);

return menu;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
package com.marginallyclever.makelangelo.makeart.turtletool;

import com.marginallyclever.makelangelo.Translator;
import com.marginallyclever.makelangelo.turtle.MovementType;
import com.marginallyclever.makelangelo.turtle.Turtle;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.operation.linemerge.LineMerger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

public class LeastHops extends TurtleTool {
private static final Logger logger = LoggerFactory.getLogger(LeastHops.class);

private final GeometryFactory geometryFactory = new GeometryFactory();
private final Set<LineString> lineSet = new HashSet<>(); // Store all lines in the drawing
private final List<LineString> mergedLines = new ArrayList<>(); // Merged lines after processing
private final Map<Point, List<Point>> graph = new HashMap<>(); // Directed gra

public LeastHops() {
super(Translator.get("LeastHops.title"));
}

@Override
public Turtle run(Turtle turtle) {
lineSet.clear();
mergedLines.clear();
graph.clear();

addLinesToJTS(turtle);

if(hasMultipleComponents()) {
logger.info("Multiple islands detected. Conversion halted.");
return turtle;
}

var path = findBestPath();
if(path.isEmpty()) {
logger.info("Found a path with zero lines?");
return turtle;
}

//var newTurtle = convertLineStringsToTurtle();
var newTurtle = convertPathToTurtle(path);

return newTurtle;
}

private Turtle convertLineStringsToTurtle() {
var newTurtle = new Turtle();
for( var p : mergedLines ) {
convertOneLineStringToTurtle(newTurtle,p);
}
return newTurtle;
}

private void convertOneLineStringToTurtle(Turtle newTurtle, LineString p) {
boolean first = true;
for( var c : p.getCoordinates() ) {
if(first) {
newTurtle.jumpTo(c.getX(), c.getY());
first = false;
} else {
newTurtle.moveTo(c.getX(), c.getY());
}
}
}

private Turtle convertPathToTurtle(List<Point> path) {
logger.debug("Converting path to turtle");
var newTurtle = new Turtle();
for (Point p0 : path) {
LineString s = findMergedLine(p0);
if(s==null) {
logger.error("Could not find line for point {}",p0);
continue;
}
convertOneLineStringToTurtle(newTurtle, s);
}
logger.debug("end");
return newTurtle;
}

private LineString findMergedLine(Point p0) {
for (LineString line : mergedLines) {
Point start = line.getStartPoint();
if(Double.compare(start.getX(), p0.getX())==0 && Double.compare(start.getY(), p0.getY())==0) {
return line;
}
}
return null;
}

// Method to convert the Eulerian path back into a detailed Turtle drawing
private Turtle convertPathToTurtle2(List<LineString> eulerianLineStrings) {
var newTurtle = new Turtle();

// Start by jumping to the beginning of the first line
LineString firstLine = eulerianLineStrings.get(0);
Coordinate[] firstCoordinates = firstLine.getCoordinates();
newTurtle.jumpTo(firstCoordinates[0].getX(), firstCoordinates[0].getY());
newTurtle.penDown();

// Traverse each LineString fully
for (LineString line : eulerianLineStrings) {
Coordinate[] coordinates = line.getCoordinates();
for (Coordinate coord : coordinates) {
newTurtle.moveTo(coord.getX(), coord.getY());
}
}

newTurtle.penUp(); // Lift the pen at the end
return newTurtle;
}

private void addLinesToJTS(Turtle turtle) {
Coordinate previous = new Coordinate(0,0);
for( var move : turtle.history ) {
if(move.type == MovementType.DRAW_LINE) {
Coordinate current = new Coordinate(move.x,move.y);
addLine( geometryFactory.createLineString( new Coordinate[]{new Coordinate(previous),current}) );
}
previous.setX(move.x);
previous.setY(move.y);
}
}

// Add a line to the set
public void addLine(LineString line) {
this.lineSet.add(line);
}

// Find the best path
public List<Point> findBestPath() {
// Step 1: Merge lines where necessary to build the graph
mergeLinesAndBuildGraph();
addRetracesForOddVertices();
// Step 2: Find Eulerian path or circuit
return eulerianPath();
}

// Merge lines and build the graph of intersections
private void mergeLinesAndBuildGraph() {
try {
LineMerger lineMerger = new LineMerger();
lineMerger.add(this.lineSet);
mergedLines.addAll(lineMerger.getMergedLineStrings());

// Step 3: Add vertices (endpoints/intersections) to the graph
for (LineString line : mergedLines) {
Point start = line.getStartPoint();
Point end = line.getEndPoint();
graph.computeIfAbsent(start, k -> new ArrayList<>()).add(end);
graph.computeIfAbsent(end, k -> new ArrayList<>()).add(start);
}
} catch (TopologyException e) {
System.err.println("Error merging lines: " + e.getMessage());
}
}

private void addRetracesForOddVertices() {
// Step 1: Identify odd-degree vertices
List<Point> oddVertices = new ArrayList<>();
for (Point vertex : graph.keySet()) {
if (graph.get(vertex).size() % 2 != 0) {
oddVertices.add(vertex);
}
}

// Step 2: If there are more than 2 odd vertices, add retraces until only 2 remain
if (oddVertices.size() > 2) {
while (oddVertices.size() > 2) {
Point v1 = oddVertices.remove(0);
Point bestMatch = null;
double bestDistance = Double.MAX_VALUE;

// Find the closest odd vertex to v1
for (Point v2 : oddVertices) {
double distance = v1.distance(v2); // Calculate distance
if (distance < bestDistance) {
bestDistance = distance;
bestMatch = v2;
}
}

if (bestMatch != null) {
oddVertices.remove(bestMatch);

// Step 3: Add retrace (imaginary edge) to make the degrees even
graph.computeIfAbsent(v1, k -> new ArrayList<>()).add(bestMatch);
graph.computeIfAbsent(bestMatch, k -> new ArrayList<>()).add(v1);

System.out.println("Added retrace between: " + v1 + " and " + bestMatch);
}
}
}

// After this step, there should be exactly 2 odd-degree vertices, which will be the start and end points.
}

// Assuming graph is now Eulerian, we use Hierholzer's algorithm to find the path.
private List<Point> eulerianPath() {
Stack<Point> stack = new Stack<>();
List<Point> path = new ArrayList<>();

// Choose one of the odd-degree vertices
stack.push(findStartVertex());

while (!stack.isEmpty()) {
Point current = stack.peek();
var node = graph.get(current);
if (node.isEmpty()) {
// If no more edges, add to path
path.add(current);
stack.pop();
} else {
// Continue to traverse edges
Point next = node.remove(0); // Get the next vertex
graph.get(next).remove(current); // Remove the reverse edge as well
stack.push(next);
}
}

// The currentPath will contain the sequence of points for the Turtle to follow
Collections.reverse(path);
return path;
}

// Helper method to find a vertex with an odd degree to start (or any vertex if all are even)
private Point findStartVertex() {
for (Point vertex : graph.keySet()) {
if (graph.get(vertex).size() % 2 != 0) {
return vertex; // Start at an odd-degree vertex
}
}
return graph.keySet().iterator().next(); // Default to any vertex if all are even
}

private boolean hasMultipleComponents() {
Set<Point> visited = new HashSet<>();
boolean foundFirstComponent = false;

// go through all vertexes
for (Point vertex : graph.keySet()) {
// if we hit one that isn't visited (might be the first, might not)
if (!visited.contains(vertex)) {
if (foundFirstComponent) {
// Found a second island (unvisited area) so quit.
return true;
}

// Explore the first component
exploreComponent(vertex, visited);
foundFirstComponent = true;
}
}

// If we finish and only found one component, return false
return false;
}

// Depth-First Search to explore a connected component
private void exploreComponent(Point start, Set<Point> visited) {
Stack<Point> stack = new Stack<>();
stack.push(start);

while (!stack.isEmpty()) {
Point vertex = stack.pop();
if (!visited.contains(vertex)) {
visited.add(vertex);
for (Point neighbor : graph.get(vertex)) {
if (!visited.contains(neighbor)) {
stack.push(neighbor);
}
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.marginallyclever.makelangelo.makeart.turtletool;

import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;

public class LineStringPlus extends LineString {
private final Point start, end;

public LineStringPlus(LineString v) {
super(v.getCoordinateSequence(), v.getFactory());
start = super.getStartPoint();
end = super.getEndPoint();
}

public Point getStartPoint() {
return start;
}

public Point getEndPoint() {
return end;
}
}
1 change: 1 addition & 0 deletions src/main/resources/languages/english.xml
Original file line number Diff line number Diff line change
Expand Up @@ -449,4 +449,5 @@
<string><key>LineWeightByImageIntensity.thickness</key><value>Thickness</value></string>
<string><key>LineWeightByImageIntensity.image</key><value>Image</value></string>

<string><key>LeastHops.title</key><value>Least Hops</value></string>
</language>

0 comments on commit ef15130

Please sign in to comment.