diff --git a/README.md b/README.md
index 82dfb46..47eed9f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# JMedialAxis
-Medial Axes for JTS Geometry
+Medial Axes (2D skeletons) for JTS Geometry
## Overview
@@ -8,7 +8,6 @@ Medial Axes for JTS Geometry
The library models the medial axis of a given geometry as a (rooted) tree of medial disks. The result is a medial axis that can be traversed recursively by starting at the largest
disk and following the child nodes until they reach the boundary of the geometry.
-Medial disks reference a parent and have up to 3 children (disks with more than one child represent bifurcation of the axis). The root of the tree is the disk whose underlying triangle has the largest circumcircle; this is also the single disk that trifurcates.
The library is geared towards medial axis visualisation & animation and enables easy and powerful navigation of medial axes.
@@ -17,16 +16,21 @@ by Roger C. Tam.
## Features
-* JTS Geometry as input
+* JTS geometry as input
* For a given medial disk, easy access to its:
* Descendants
* Ancestors
- * Belonging segment
+ * Belonging branch
* Parent disk
- * Children disks
-* Branch/Segment pruning, via:
+ * Child disk(s)
+* Branch pruning, via:
* Feature area pruning
* Supports geometries with holes (genus > 0)
+* Output as:
+ * List of edges
+ * List of segments
+ * Dissolved (simplified) JTS geometry
+ * JTS LineMergeGraph
## Showcase
...
diff --git a/src/main/java/micycle/medialAxis/MedialAxis.java b/src/main/java/micycle/medialAxis/MedialAxis.java
index 57ee0da..483ea78 100644
--- a/src/main/java/micycle/medialAxis/MedialAxis.java
+++ b/src/main/java/micycle/medialAxis/MedialAxis.java
@@ -53,7 +53,7 @@
*
* Decompose the disk coordinates into curves.
*
- * If angle of successive line segments is the same, merge them
+ * If angle of successive line branches is the same, merge them
*
* * One way to get a smoother initial axis (i.e. without pruning) is to apply
* on a smoothed version of the shape.
@@ -67,20 +67,22 @@ public class MedialAxis {
new PrecisionModel(PrecisionModel.FLOATING_SINGLE));
private Geometry g;
-// private final PreparedGeometry cache;
- private KDTree kdTree;
-// private IncrementalTin tin;
-// private TC tc;
+ private KDTree kdTree;
+
private Geometry dissolved;
+ private LineMergeGraph lineMergeGraph;
+
+ private final ArrayList voronoiDisks = new ArrayList<>();
+ public MedialDisk rootNode; // the medial axis has a trifuraction at the root
+ MedialDisk deepestNode;
+ private List leaves;
+ private List bifurcations; // birfurcating/forking nodes
+ private List branches;
+ private final List edges;
- ArrayList voronoiDisks = new ArrayList<>();
- public VD rootNode; // the medial axis has a trifuraction at the root
- VD deepestNode;
- private List leaves;
- private List bifurcations; // birfurcating/forking nodes
- private List segments;
+ public boolean debug = false;
-// public ArrayList> segments = new ArrayList<>(); // node sections between forks
+// public ArrayList> branches = new ArrayList<>(); // node sections between forks
public MedialAxis(Coordinate[] coordinates) {
// TODO support holes
@@ -108,7 +110,9 @@ public MedialAxis(Geometry g) {
/**
* Simplify if complex
*/
-// System.out.println("before " + g.getCoordinates().length);
+ if (debug) {
+ System.out.println("Input coordinates #: " + g.getCoordinates().length);
+ }
if (g.getCoordinates().length > 2000) {
Polygon poly = (Polygon) g;
if (poly.getNumInteriorRing() > 0) {
@@ -116,72 +120,83 @@ public MedialAxis(Geometry g) {
} else {
g = DouglasPeuckerSimplifier.simplify(g, 1);
}
+ if (debug) {
+ System.out.println("Simplifying shape. New coordinates #: " + g.getCoordinates().length);
+ }
}
-// System.out.println("w/simplify " + g.getCoordinates().length);
pointLocator = new IndexedPointInAreaLocator(g);
this.g = Densifier.densify(g, 10); // NOTE constant=10
-// System.out.println("after " + this.g.getCoordinates().length);
+ if (debug) {
+ System.out.println("Densified coordinates #" + this.g.getCoordinates().length);
+ }
TC tc = prepareTin();
+ // TODO clustering after tin generation
// having done triangulation, build up directed tree of medial axis
- rootNode = new VD(null, 0, tc.largestDiskTriangle, tc.largestCircumcircle, 0);
- deepestNode = rootNode;
- ArrayDeque stack = new ArrayDeque<>(15);
+ rootNode = new MedialDisk(null, 0, tc.largestDiskTriangle, tc.largestCircumcircle, 0);
+ ArrayDeque stack = new ArrayDeque<>(15);
stack.push(rootNode); // start tree with rootnode
- HashSet remaining = new HashSet<>();
+ HashSet remaining = new HashSet<>(); // use bit set?
remaining.addAll(tc.triangles);
remaining.remove(tc.largestDiskTriangle);
+ edges = new ArrayList();
+
int depth = 1; // number of edges in the path from the root to the node
int id = 1; // breadth-first ID
while (!stack.isEmpty()) {
- VD live = stack.pop(); // FIFO (BFS)
+ MedialDisk live = stack.pop(); // FIFO (BFS)
voronoiDisks.add(live);
- // get half-edge duals (shared with other triangles)
+ // get half-edge duals (edge shared with other triangles)
final SimpleTriangle n1 = tc.map.get(live.t.getEdgeA().getDual());
final SimpleTriangle n2 = tc.map.get(live.t.getEdgeB().getDual());
final SimpleTriangle n3 = tc.map.get(live.t.getEdgeC().getDual());
if (remaining.contains(n1)) {
- VD child = new VD(live, id++, n1, tc.cccMap.get(n1), depth);
+ MedialDisk child = new MedialDisk(live, id++, n1, tc.cccMap.get(n1), depth);
stack.add(child);
live.addchild(child);
remaining.remove(n1);
+ edges.add(new Edge(live, child));
}
if (remaining.contains(n2)) {
- VD child = new VD(live, id++, n2, tc.cccMap.get(n2), depth);
+ MedialDisk child = new MedialDisk(live, id++, n2, tc.cccMap.get(n2), depth);
stack.add(child);
live.addchild(child);
remaining.remove(n2);
+ edges.add(new Edge(live, child));
}
if (remaining.contains(n3)) {
- VD child = new VD(live, id++, n3, tc.cccMap.get(n3), depth);
+ MedialDisk child = new MedialDisk(live, id++, n3, tc.cccMap.get(n3), depth);
stack.add(child);
live.addchild(child);
remaining.remove(n3);
+ edges.add(new Edge(live, child));
}
depth++;
}
+
deepestNode = voronoiDisks.get(voronoiDisks.size() - 1); // TODO check
// calculateFeatureArea();
- calcFeatureArea();
-// System.out.println("total area: " + totalArea);
-// System.out.println(rootNode.featureArea);
+// calcFeatureArea();
+ if (debug) {
+ System.out.println("Total Area: " + rootNode.featureArea);
+ }
}
- public List getDisks() {
+ public List getDisks() {
return voronoiDisks;
}
- public VD nearestDisk(double x, double y) {
- // TODO use cover tree?
+ public MedialDisk nearestDisk(double x, double y) {
+ // TODO use PhTree/Quadtree?
if (kdTree == null) { // Lazy initialisation
kdTree = KDTree.create(2);
voronoiDisks.forEach(disk -> kdTree.insert(new double[] { disk.position.x, disk.position.y }, disk));
@@ -189,6 +204,10 @@ public VD nearestDisk(double x, double y) {
return kdTree.nnQuery(new double[] { x, y }).value();
}
+ public MedialDisk nearestDisk(Coordinate coordinate) {
+ return nearestDisk(coordinate.x, coordinate.y);
+ }
+
/**
* Returns the children of a disk into a linear array
*
@@ -196,15 +215,15 @@ public VD nearestDisk(double x, double y) {
* position 0)
* @return
*/
- public List getDescendants(VD parent) {
+ public List getDescendants(MedialDisk parent) {
- ArrayDeque stack = new ArrayDeque<>();
+ ArrayDeque stack = new ArrayDeque<>();
stack.add(parent);
- ArrayList out = new ArrayList<>();
+ ArrayList out = new ArrayList<>();
while (!stack.isEmpty()) {
- VD live = stack.pop();
+ MedialDisk live = stack.pop();
out.add(live);
live.children.forEach(child -> {
stack.add(child);
@@ -213,16 +232,16 @@ public List getDescendants(VD parent) {
return out;
}
- public List getDescendants(VD parent, int maxDepth) {
+ public List getDescendants(MedialDisk parent, int maxDepth) {
- ArrayDeque stack = new ArrayDeque<>();
+ ArrayDeque stack = new ArrayDeque<>();
stack.add(parent);
- ArrayList out = new ArrayList<>();
+ ArrayList out = new ArrayList<>();
int depth = 0;
while (!stack.isEmpty() && depth < maxDepth) {
- VD live = stack.pop();
+ MedialDisk live = stack.pop();
out.add(live);
live.children.forEach(child -> {
stack.add(child);
@@ -243,11 +262,11 @@ public List getDescendants(VD parent, int maxDepth) {
* @param child
* @return
*/
- public List getAncestors(VD child) {
+ public List getAncestors(MedialDisk child) {
- ArrayList out = new ArrayList<>();
+ ArrayList out = new ArrayList<>();
- VD live = child;
+ MedialDisk live = child;
while (live.parent != null) {
out.add(live);
live = live.parent;
@@ -255,13 +274,41 @@ public List getAncestors(VD child) {
return out;
}
- public void getDissolvedGeometry() {
- LineDissolver ld = new LineDissolver();
- // dissolved geom
+ /**
+ * Returns a JTS geometry where medial axis edges are dissolved into a set of
+ * maximal-length Linestrings. The output can be further simplfied with
+ * DouglasPeuckerSimplifier for example.
+ *
+ * @return
+ */
+ public Geometry getDissolvedGeometry() {
+ if (dissolved == null) { // Lazy initialisation
+ LineDissolver ld = new LineDissolver();
+ edges.forEach(e -> {
+ ld.add(e.lineString);
+ });
+ dissolved = ld.getResult();
+// System.out.println(String.format("Dissolved %s axis edges into %s linestrings.", edges.size(),
+// dissolved.getNumGeometries()));
+ }
+ return dissolved;
}
- public void getLineMergeGraph() {
- LineMergeGraph lmGraph = new LineMergeGraph();
+ /**
+ * Get the medial axis in the form of an undirected planar graph. The graph is
+ * based on the dissolved geometry.
+ *
+ * @return
+ */
+ public LineMergeGraph getLineMergeGraph() {
+ if (lineMergeGraph == null) { // Lazy initialisation
+ getDissolvedGeometry();
+ lineMergeGraph = new LineMergeGraph();
+ for (int i = 0; i < dissolved.getNumGeometries(); i++) {
+ lineMergeGraph.addEdge((LineString) dissolved.getGeometryN(i));
+ }
+ }
+ return lineMergeGraph;
}
/**
@@ -269,7 +316,7 @@ public void getLineMergeGraph() {
*
* @return
*/
- public List getLeaves() {
+ public List getLeaves() {
if (leaves == null) { // Lazy initialisation
leaves = voronoiDisks.stream().filter(vd -> vd.degree == 0).collect(Collectors.toList());
}
@@ -281,44 +328,49 @@ public List getLeaves() {
*
* @return
*/
- public List getBifurcations() {
+ public List getBifurcations() {
if (bifurcations == null) { // Lazy initialisation
bifurcations = voronoiDisks.stream().filter(vd -> vd.degree == 2).collect(Collectors.toList());
}
return bifurcations;
}
- public void getEdges() {
+ /**
+ *
+ * @return list of edges (in breadth-first order from the root node) connecting
+ * medial disks.
+ */
+ public List getEdges() {
+ return edges;
}
/**
* Aka features, aka branches Segment is a linear portion of medial disks.
*
- * @return List of {@link micycle.medialAxis.MedialAxis.Segment segments}
+ * @return List of {@link micycle.medialAxis.MedialAxis.Branch branches}
*/
- public List getSegments() {
- if (segments == null) { // Lazy initialisation
- segments = new ArrayList<>();
+ public List getBranches() {
+ if (branches == null) { // Lazy initialisation
+ branches = new ArrayList<>();
- Segment segment;
- ArrayList forks = new ArrayList<>(getBifurcations());
+ Branch branch;
+ ArrayList forks = new ArrayList<>(getBifurcations());
forks.add(rootNode);
- for (VD disk : forks) {
- for (VD child : disk.children) {
- segment = new Segment(disk); // init segment with bifurcating node
- VD node = child;
+ for (MedialDisk disk : forks) {
+ for (MedialDisk child : disk.children) {
+ branch = new Branch(disk); // init branch with bifurcating node
+ MedialDisk node = child;
while (node.degree == 1) {
- segment.add(node);
+ branch.add(node);
node = node.children.get(0); // get the only child node
}
- segment.end(node); // end with axis leaf or next bifurcating node
- segments.add(segment);
+ branch.end(node); // end with axis leaf or next bifurcating node
+ branches.add(branch);
}
}
}
-
- return this.segments;
+ return this.branches;
}
/**
@@ -333,7 +385,7 @@ private TC prepareTin() {
// Triangulate geometry coordinates
Coordinate[] coords = g.getCoordinates();
final ArrayList vertices = new ArrayList<>();
- for (int i = 0; i < coords.length; i++) {
+ for (int i = 0; i < coords.length - 1; i++) {
vertices.add(new Vertex(coords[i].x, coords[i].y, 0));
}
tin.add(vertices, null); // insert point set; points are triangulated upon insertion
@@ -356,21 +408,21 @@ private void calculateDepthFirstIndices() {
private void calculateFeatureArea() {
/**
- * Starting at leaves, sum each segment, stopping when reach bifurcating node;
+ * Starting at leaves, sum each branch, stopping when reach bifurcating node;
* add bifurcation node to array and use that next time.
*/
- List nodes = getLeaves();
- HashSet nextLeaves = new HashSet<>(); // what to begin with next iteration
- HashMap waitAtNodes = new HashMap<>();
+ List nodes = getLeaves();
+ HashSet nextLeaves = new HashSet<>(); // what to begin with next iteration
+ HashMap waitAtNodes = new HashMap<>();
getBifurcations().forEach(n -> waitAtNodes.put(n, 0));
while (!nodes.isEmpty()) {
- for (VD node : nodes) {
+ for (MedialDisk node : nodes) {
- VD current = node;
+ MedialDisk current = node;
while (current.degree < 2) { // FIXME what happens when start with bifurcating node
current.parent.featureArea += current.featureArea;
@@ -400,7 +452,7 @@ private void calculateFeatureArea() {
}
}
- nodes = new ArrayList(nextLeaves);
+ nodes = new ArrayList(nextLeaves);
nextLeaves.clear();
}
}
@@ -411,11 +463,11 @@ private void calcFeatureArea() {
}
}
- private static double recurseFeatureArea(VD node) {
+ private static double recurseFeatureArea(MedialDisk node) {
if (node.degree == 0) {
return node.area;
}
- for (VD child : node.children) {
+ for (MedialDisk child : node.children) {
node.featureArea += recurseFeatureArea(child);
}
return node.featureArea;
@@ -425,7 +477,7 @@ public void drawVDM(PApplet p) {
// looks different when drawing with DFS vs BFS
- ArrayDeque stack = new ArrayDeque<>();
+ ArrayDeque stack = new ArrayDeque<>();
stack.push(rootNode);
p.strokeWeight(5);
p.colorMode(PConstants.HSB, 1, 1, 1, 1);
@@ -435,7 +487,7 @@ public void drawVDM(PApplet p) {
final float depth = deepestNode.depthBF;
while (!stack.isEmpty()) {
- VD live = stack.pop();
+ MedialDisk live = stack.pop();
live.children.forEach(child -> {
p.stroke((live.depthBF / depth) * .8f, 1, 1, 0.8f);
@@ -456,7 +508,7 @@ public void drawVDM(PApplet p, int maxDepth) {
// iterate by DFS for easier break
- ArrayDeque stack = new ArrayDeque<>();
+ ArrayDeque stack = new ArrayDeque<>();
stack.push(rootNode);
p.strokeWeight(5);
p.colorMode(PConstants.HSB, 1, 1, 1, 1);
@@ -466,7 +518,7 @@ public void drawVDM(PApplet p, int maxDepth) {
final float depth = deepestNode.depthBF;
while (!stack.isEmpty()) {
- VD live = stack.pop();
+ MedialDisk live = stack.pop();
// Vertex v = live.t.getVertexA();
// p.beginShape();
@@ -522,7 +574,7 @@ public void drawVDMPrune(PApplet p, double threshold) {
// iterate by DFS for easier break
- ArrayDeque stack = new ArrayDeque<>();
+ ArrayDeque stack = new ArrayDeque<>();
stack.push(rootNode);
p.strokeWeight(5);
p.colorMode(PConstants.HSB, 1, 1, 1, 1);
@@ -532,7 +584,7 @@ public void drawVDMPrune(PApplet p, double threshold) {
final float depth = deepestNode.depthBF;
while (!stack.isEmpty()) {
- VD live = stack.pop();
+ MedialDisk live = stack.pop();
live.children.forEach(child -> {
if (child.featureArea > areaLimit) {
@@ -546,29 +598,6 @@ public void drawVDMPrune(PApplet p, double threshold) {
p.colorMode(PConstants.RGB, 255, 255, 255, 255);
}
- /**
- * @return double[3] of [x, y, radius]
- */
- private static double[] circumcircle(Vertex a, Vertex b, Vertex c) {
-
- double D = (a.getX() - c.getX()) * (b.getY() - c.getY()) - (b.getX() - c.getX()) * (a.getY() - c.getY());
- double px = (((a.getX() - c.getX()) * (a.getX() + c.getX()) + (a.getY() - c.getY()) * (a.getY() + c.getY())) / 2
- * (b.getY() - c.getY())
- - ((b.getX() - c.getX()) * (b.getX() + c.getX()) + (b.getY() - c.getY()) * (b.getY() + c.getY())) / 2
- * (a.getY() - c.getY()))
- / D;
-
- double py = (((b.getX() - c.getX()) * (b.getX() + c.getX()) + (b.getY() - c.getY()) * (b.getY() + c.getY())) / 2
- * (a.getX() - c.getX())
- - ((a.getX() - c.getX()) * (a.getX() + c.getX()) + (a.getY() - c.getY()) * (a.getY() + c.getY())) / 2
- * (b.getX() - c.getX()))
- / D;
-
- double rs = (c.getX() - px) * (c.getX() - px) + (c.getY() - py) * (c.getY() - py);
-
- return new double[] { px, py, Math.sqrt(rs) };
- }
-
private class TC implements Consumer {
SimpleTriangle largestDiskTriangle = null; // triangle with largest circumcircle
@@ -629,17 +658,17 @@ public void accept(SimpleTriangle t) {
* @author MCarleton
*
*/
- public static class VD {
+ public static class MedialDisk {
/** The underlying delaunay triangle associated with this disk */
public SimpleTriangle t;
/** This disk's parent node. Null if root node */
- public VD parent;
+ public MedialDisk parent;
/**
* This disk's children nodes. Nodes have upto 3 children. Leaf nodes have 0
* children.
*/
- ArrayList children;
+ ArrayList children;
public final int depthBF; // breadth-first depth from the root node, better for drawing. // TODO
public int depthDF; // distance from the root, length of the path from n to the root TODO
@@ -651,18 +680,18 @@ public static class VD {
/** The sum of triangle areas of this disk and all its descendants */
public double featureArea;
- VD segmentParent;
+ MedialDisk branchParent; // TODO
-// "segment"/"path" b // segment is the set of VDs between two nodes of degree > 1, or between a node of degree >1 and a leaf
- // segment should point to start and end
+// "branch"/"path" b // branch is the set of VDs between two nodes of degree > 1, or between a node of degree >1 and a leaf
+ // branch should point to start and end
- boolean forkChild = false; // is direct child of a bifurcating disk OR "issegmentParent"
+ boolean forkChild = false; // is direct child of a bifurcating disk OR "isbranchParent"
boolean forkParent = false; // (is child a node of degree >1?)
/**
* Measures the change in the width of the shape per unit length of the axis.If
* positive, this part of the object widens as we progress along the axis
- * segment; if it’s negative the part narrows
+ * branch; if it’s negative the part narrows
*/
final double axialGradient; // axial gradient between this and its parent. (rChild-rParent/d)
@@ -672,13 +701,12 @@ public static class VD {
final int id; // BFS id from root node, unique for each disk (unlike depthDF)
- VD(VD parent, int id, SimpleTriangle t, double[] circumcircle, int depthBF) {
+ MedialDisk(MedialDisk parent, int id, SimpleTriangle t, double[] circumcircle, int depthBF) {
this.parent = parent;
this.id = id;
this.t = t;
children = new ArrayList<>(3);
this.depthBF = depthBF;
-// this.circumcircle = circumcircle;
area = t.getArea();
featureArea = area;
position = new Coordinate(circumcircle[0], circumcircle[1]);
@@ -693,7 +721,7 @@ public static class VD {
/**
* Add a child disk to this disk and increment its degree.
*/
- private void addchild(VD child) {
+ private void addchild(MedialDisk child) {
children.add(child);
degree++;
}
@@ -710,59 +738,58 @@ public int hashCode() {
}
/**
- * Edges model links between two disks. Return graphs based on merged edges
- *
- * @author MCarleton
- *
+ * An edge models a link between two disks.
*/
public static class Edge {
- public final VD head;
- public final VD tail;
+ public final MedialDisk head;
+ public final MedialDisk tail;
int depth;
- final double axialGradient;
+ public final double axialGradient;
+ public final LineString lineString;
- public Edge(VD head, VD tail) {
+ public Edge(MedialDisk head, MedialDisk tail) {
this.head = head;
this.tail = tail;
+ lineString = GEOM_FACTORY.createLineString(new Coordinate[] { head.position, tail.position });
depth = head.depthBF;
axialGradient = tail.axialGradient;
}
}
/**
- * segment/trunk segment
+ * branch/trunk branch
*
* comparable based on fork degree?
*
*/
- public static class Segment {
+ public static class Branch {
- public final VD root; // root disk
- public VD leaf; // leaf disk
+ public final MedialDisk root; // root disk
+ public MedialDisk leaf; // leaf disk
/**
- * Disks between root and leaf of this segment, in descending order from the
- * segment's root.
+ * Disks between root and leaf of this branch, in descending order from the
+ * branch's root.
*/
- public List innerDisks;
- Segment sibling; // segment that shares parent disk
+ public List innerDisks;
+ Branch sibling; // branch that shares parent disk
// contains bezier interpolation
- int forkDegree; // how many forks are visited from the rootnode to the root of this segment
+ int forkDegree; // how many forks are visited from the rootnode to the root of this branch
double length; // sum of edge lengths (not count)
public final List edges;
- public LineString lineString;
+ public LineString lineString; // TODO
/**
*
* @param root a tri/bifurcating disk
*/
- Segment(VD root) {
+ Branch(MedialDisk root) {
this.root = root;
innerDisks = new ArrayList<>();
edges = new ArrayList();
}
- void add(VD disk) {
+ void add(MedialDisk disk) {
if (innerDisks.size() == 0) {
edges.add(new Edge(root, disk));
} else {
@@ -772,11 +799,11 @@ void add(VD disk) {
}
/**
- * Add the last (leaf) disk for the segment
+ * Add the last (leaf) disk for the branch
*
* @param disk
*/
- void end(VD disk) {
+ void end(MedialDisk disk) {
leaf = disk;
if (innerDisks.isEmpty()) { // bifurcations that link directly to other bifucations
edges.add(new Edge(root, disk));
@@ -795,6 +822,33 @@ void end(VD disk) {
void smooth(int smoothingType) {
// TODO
}
+
+ public boolean terminates() {
+ return root.isLeaf();
+ }
+ }
+
+ /**
+ * @return double[3] of [x, y, radius]
+ */
+ private static double[] circumcircle(Vertex a, Vertex b, Vertex c) {
+
+ double D = (a.getX() - c.getX()) * (b.getY() - c.getY()) - (b.getX() - c.getX()) * (a.getY() - c.getY());
+ double px = (((a.getX() - c.getX()) * (a.getX() + c.getX()) + (a.getY() - c.getY()) * (a.getY() + c.getY())) / 2
+ * (b.getY() - c.getY())
+ - ((b.getX() - c.getX()) * (b.getX() + c.getX()) + (b.getY() - c.getY()) * (b.getY() + c.getY())) / 2
+ * (a.getY() - c.getY()))
+ / D;
+
+ double py = (((b.getX() - c.getX()) * (b.getX() + c.getX()) + (b.getY() - c.getY()) * (b.getY() + c.getY())) / 2
+ * (a.getX() - c.getX())
+ - ((a.getX() - c.getX()) * (a.getX() + c.getX()) + (a.getY() - c.getY()) * (a.getY() + c.getY())) / 2
+ * (b.getX() - c.getX()))
+ / D;
+
+ double rs = (c.getX() - px) * (c.getX() - px) + (c.getY() - py) * (c.getY() - py);
+
+ return new double[] { px, py, Math.sqrt(rs) };
}
private static double distance(Coordinate a, Coordinate b) {