@@ -62,6 +62,19 @@ public class GraphDrawingQualityAnalyzer {
6262 // ── BFS distance cache ──────────────────────────────────────────
6363 private Map <String , Map <String , Integer >> distCache ;
6464
65+ // ── Pre-computed spatial data (shared across stress/overlap/neighbourhood) ──
66+ /** Positioned vertices in stable order. */
67+ private List <String > posVerts ;
68+ /** posVerts.size(). */
69+ private int posN ;
70+ /** x[i], y[i] coordinates indexed by posVerts order. */
71+ private double [] posX , posY ;
72+ /** Flat upper-triangle Euclidean distance matrix: dist(i,j) for i<j
73+ * stored at index i*posN - i*(i+1)/2 + (j-i-1). */
74+ private double [] pairDist ;
75+ /** Maps vertex ID → index in posVerts (for fast lookup). */
76+ private Map <String , Integer > posIdx ;
77+
6578 public GraphDrawingQualityAnalyzer (Graph <String , Edge > graph ,
6679 Map <String , Point2D > positions ) {
6780 this .graph = Objects .requireNonNull (graph );
@@ -140,6 +153,7 @@ public String generateReport() {
140153
141154 private synchronized void ensureComputed () {
142155 if (computed ) return ;
156+ buildSpatialIndex ();
143157 computeEdgeCrossings ();
144158 computeEdgeLengths ();
145159 computeAngularResolution ();
@@ -151,6 +165,48 @@ private synchronized void ensureComputed() {
151165 computed = true ;
152166 }
153167
168+ /**
169+ * Pre-computes positioned vertex list, coordinate arrays, index map,
170+ * and pairwise Euclidean distance matrix. This is done once and shared
171+ * by computeStress, computeNeighbourhoodPreservation, and
172+ * computeOverlapRatio — eliminating three independent O(V²) passes
173+ * that each performed HashMap lookups and Point2D.distance() calls.
174+ */
175+ private void buildSpatialIndex () {
176+ posVerts = new ArrayList <>();
177+ for (String v : graph .getVertices ()) {
178+ if (positions .containsKey (v )) posVerts .add (v );
179+ }
180+ posN = posVerts .size ();
181+ posX = new double [posN ];
182+ posY = new double [posN ];
183+ posIdx = new HashMap <>(posN * 2 );
184+ for (int i = 0 ; i < posN ; i ++) {
185+ Point2D p = positions .get (posVerts .get (i ));
186+ posX [i ] = p .getX ();
187+ posY [i ] = p .getY ();
188+ posIdx .put (posVerts .get (i ), i );
189+ }
190+ // Flat upper-triangle distance matrix
191+ long triSize = (long ) posN * (posN - 1 ) / 2 ;
192+ pairDist = new double [(int ) triSize ];
193+ int idx = 0 ;
194+ for (int i = 0 ; i < posN ; i ++) {
195+ double xi = posX [i ], yi = posY [i ];
196+ for (int j = i + 1 ; j < posN ; j ++) {
197+ double dx = xi - posX [j ];
198+ double dy = yi - posY [j ];
199+ pairDist [idx ++] = Math .sqrt (dx * dx + dy * dy );
200+ }
201+ }
202+ }
203+
204+ /** Returns the pre-computed Euclidean distance between posVerts[i] and posVerts[j] (i < j). */
205+ private double spatialDist (int i , int j ) {
206+ if (i > j ) { int t = i ; i = j ; j = t ; }
207+ return pairDist [i * posN - i * (i + 1 ) / 2 + (j - i - 1 )];
208+ }
209+
154210 // ── Edge crossings ──────────────────────────────────────────────
155211
156212 private void computeEdgeCrossings () {
@@ -253,27 +309,30 @@ private void computeAngularResolution() {
253309
254310 // ── Stress (Kamada-Kawai) ───────────────────────────────────────
255311
312+ /**
313+ * Kamada-Kawai stress using pre-computed spatial distance matrix.
314+ * Avoids per-pair HashMap lookups and Point2D.distance() calls.
315+ */
256316 private void computeStress () {
257- List <String > verts = new ArrayList <>();
258- for (String v : graph .getVertices ()) {
259- if (positions .containsKey (v )) verts .add (v );
260- }
261- if (verts .size () < 2 ) { stress = 0 ; return ; }
317+ if (posN < 2 ) { stress = 0 ; return ; }
262318
263- ensureDistCache (verts );
319+ ensureDistCache (posVerts );
264320
265321 double totalStress = 0 ;
266322 double normaliser = 0 ;
267- for (int i = 0 ; i < verts .size (); i ++) {
268- for (int j = i + 1 ; j < verts .size (); j ++) {
269- String u = verts .get (i ), v = verts .get (j );
270- Integer dij = distCache .getOrDefault (u , Collections .emptyMap ()).get (v );
323+ for (int i = 0 ; i < posN ; i ++) {
324+ String u = posVerts .get (i );
325+ Map <String , Integer > uDist = distCache .get (u );
326+ if (uDist == null ) continue ;
327+ for (int j = i + 1 ; j < posN ; j ++) {
328+ Integer dij = uDist .get (posVerts .get (j ));
271329 if (dij == null || dij == 0 ) continue ;
272330
273- double drawDist = positions .get (u ).distance (positions .get (v ));
274- double ideal = dij * edgeLengthMean ; // scale graph distance by mean edge length
275- double w = 1.0 / (dij * dij );
276- totalStress += w * (drawDist - ideal ) * (drawDist - ideal );
331+ double drawDist = spatialDist (i , j );
332+ double ideal = dij * edgeLengthMean ;
333+ double w = 1.0 / ((double ) dij * dij );
334+ double diff = drawDist - ideal ;
335+ totalStress += w * diff * diff ;
277336 normaliser += w * ideal * ideal ;
278337 }
279338 }
@@ -282,43 +341,57 @@ private void computeStress() {
282341
283342 // ── Neighbourhood preservation ──────────────────────────────────
284343
344+ /**
345+ * Neighbourhood preservation using pre-computed spatial distances.
346+ *
347+ * <p>For each vertex, finds the k nearest vertices in the drawing and
348+ * checks overlap with graph neighbours. Uses array-indexed distances
349+ * from the pre-computed matrix instead of per-vertex Point2D.distance()
350+ * calls and Map.Entry allocations. Sorts an int[] of indices by distance
351+ * rather than allocating O(V) Map.Entry objects per vertex.</p>
352+ */
285353 private void computeNeighbourhoodPreservation () {
286354 int totalNeighbours = 0 ;
287355 int preserved = 0 ;
288356
289- List <String > verts = new ArrayList <>();
290- for (String v : graph .getVertices ()) {
291- if (positions .containsKey (v )) verts .add (v );
292- }
357+ // Reusable array for sorting neighbour indices by distance
358+ Integer [] sortIndices = new Integer [posN ];
359+ for (int i = 0 ; i < posN ; i ++) sortIndices [i ] = i ;
293360
294- for (String v : verts ) {
361+ for (int vi = 0 ; vi < posN ; vi ++) {
362+ String v = posVerts .get (vi );
295363 Collection <String > nbrs = graph .getNeighbors (v );
296364 if (nbrs == null || nbrs .isEmpty ()) continue ;
297365
298366 int k = 0 ;
299367 for (String n : nbrs ) {
300- if (positions .containsKey (n )) k ++;
368+ if (posIdx .containsKey (n )) k ++;
301369 }
302370 if (k == 0 ) continue ;
303371
304- // find k nearest in drawing
305- Point2D pv = positions .get (v );
306- List <Map .Entry <String , Double >> dists = new ArrayList <>();
307- for (String u : verts ) {
308- if (u .equals (v )) continue ;
309- dists .add (new AbstractMap .SimpleEntry <>(u , pv .distance (positions .get (u ))));
310- }
311- dists .sort (Comparator .comparingDouble (Map .Entry ::getValue ));
312-
313- Set <String > kNearest = new HashSet <>();
314- for (int i = 0 ; i < Math .min (k , dists .size ()); i ++) {
315- kNearest .add (dists .get (i ).getKey ());
372+ // Sort all other vertex indices by distance to vi
373+ final int src = vi ;
374+ Arrays .sort (sortIndices , (a , b ) -> {
375+ if (a == src ) return 1 ; // push self to end
376+ if (b == src ) return -1 ;
377+ return Double .compare (spatialDist (src , a ), spatialDist (src , b ));
378+ });
379+
380+ // Collect k nearest (skip self)
381+ Set <Integer > kNearest = new HashSet <>(k * 2 );
382+ int found = 0 ;
383+ for (int i = 0 ; i < posN && found < k ; i ++) {
384+ int idx = sortIndices [i ];
385+ if (idx == vi ) continue ;
386+ kNearest .add (idx );
387+ found ++;
316388 }
317389
318390 for (String n : nbrs ) {
319- if (positions .containsKey (n )) {
391+ Integer ni = posIdx .get (n );
392+ if (ni != null ) {
320393 totalNeighbours ++;
321- if (kNearest .contains (n )) preserved ++;
394+ if (kNearest .contains (ni )) preserved ++;
322395 }
323396 }
324397 }
@@ -329,22 +402,23 @@ private void computeNeighbourhoodPreservation() {
329402
330403 // ── Overlap ratio ───────────────────────────────────────────────
331404
405+ /**
406+ * Overlap ratio using pre-computed spatial distance matrix.
407+ * Direct array access replaces per-pair HashMap lookups and
408+ * Point2D.distance() calls.
409+ */
332410 private void computeOverlapRatio () {
333- List <String > verts = new ArrayList <>();
334- for (String v : graph .getVertices ()) {
335- if (positions .containsKey (v )) verts .add (v );
336- }
337- if (verts .size () < 2 ) { overlapRatio = 0 ; return ; }
411+ if (posN < 2 ) { overlapRatio = 0 ; return ; }
338412
339- // threshold: 5% of average edge length or 10 pixels, whichever is larger
340413 double threshold = Math .max (edgeLengthMean * 0.05 , 10.0 );
341414 int overlaps = 0 ;
342415 int pairs = 0 ;
416+ int idx = 0 ;
343417
344- for (int i = 0 ; i < verts . size () ; i ++) {
345- for (int j = i + 1 ; j < verts . size () ; j ++) {
418+ for (int i = 0 ; i < posN ; i ++) {
419+ for (int j = i + 1 ; j < posN ; j ++) {
346420 pairs ++;
347- if (positions . get ( verts . get ( i )). distance ( positions . get ( verts . get ( j ))) < threshold ) {
421+ if (pairDist [ idx ++] < threshold ) {
348422 overlaps ++;
349423 }
350424 }
0 commit comments