SpringForce.java
/**
* Copyright (c) 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* SPDX-License-Identifier: MPL-2.0
*/
package com.powsybl.diagram.util.layout.forces;
import com.powsybl.diagram.util.layout.forces.parameters.SpringParameter;
import com.powsybl.diagram.util.layout.geometry.LayoutContext;
import com.powsybl.diagram.util.layout.geometry.Point;
import com.powsybl.diagram.util.layout.geometry.Vector2D;
import org.jgrapht.Graphs;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.SimpleGraph;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author Nathan Dissoubray {@literal <nathan.dissoubray at rte-france.com>}
*/
public class SpringForce<V, E> implements Force<V, E> {
private final Map<DefaultEdge, SpringParameter> springs;
private static final double DEFAULT_STIFFNESS = 100.0;
public SpringForce() {
this.springs = new HashMap<>();
}
@Override
public void init(LayoutContext<V, E> layoutContext) {
SimpleGraph<V, DefaultEdge> simpleGraph = layoutContext.getSimpleGraph();
for (DefaultEdge edge : simpleGraph.edgeSet()) {
V edgeSource = simpleGraph.getEdgeSource(edge);
V edgeTarget = simpleGraph.getEdgeTarget(edge);
if (layoutContext.getFixedPoints().containsKey(edgeSource) && layoutContext.getFixedPoints().containsKey(edgeTarget)) {
continue;
}
Point pointSource = Objects.requireNonNullElseGet(layoutContext.getMovingPoints().get(edgeSource), () -> layoutContext.getInitialPoints().get(edgeSource));
Point pointTarget = Objects.requireNonNullElseGet(layoutContext.getMovingPoints().get(edgeTarget), () -> layoutContext.getInitialPoints().get(edgeTarget));
if (pointSource != pointTarget) { // no use in force layout to add loops
springs.put(edge, new SpringParameter(DEFAULT_STIFFNESS, simpleGraph.getEdgeWeight(edge)));
}
}
}
/**
* This is Hooke's Law
*/
@Override
public Vector2D apply(V vertex, Point point, LayoutContext<V, E> layoutContext) {
Vector2D resultingForce = new Vector2D(0, 0);
for (DefaultEdge edge : layoutContext.getSimpleGraph().edgesOf(vertex)) {
// this is basically what is done in Graphs.neighborSet, but we need the edge to get the corresponding spring
V otherVertex = Graphs.getOppositeVertex(layoutContext.getSimpleGraph(), edge, vertex);
Point otherPoint = layoutContext.getMovingPoints().get(otherVertex);
if (otherPoint == null) {
otherPoint = layoutContext.getFixedPoints().get(otherVertex);
}
if (otherPoint == null) {
throw new NullPointerException(String.format("No such point corresponding to the given vertex in either moving or non-moving points: Vertex %s", otherVertex));
}
SpringParameter spring = springs.get(edge);
Vector2D force = Vector2D.calculateVectorBetweenPoints(point, otherPoint);
double displacement = force.magnitude() - spring.getLength();
force.normalize();
// multiply by 0.5 because each vertex will move half of the distance, assuming both are free
// should this be different if the other point is not moving ?
force.multiplyBy(spring.getStiffness() * displacement * 0.5);
// might be good to have a method to do this in place instead of creating new Vector2D each time
resultingForce.add(force);
}
return resultingForce;
}
}