-
Notifications
You must be signed in to change notification settings - Fork 45
/
BorderNodeLayoutEngine.java
233 lines (203 loc) · 11.9 KB
/
BorderNodeLayoutEngine.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*******************************************************************************
* Copyright (c) 2022, 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.diagrams.layout.incremental;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.sirius.components.diagrams.Position;
import org.eclipse.sirius.components.diagrams.Size;
import org.eclipse.sirius.components.diagrams.events.IDiagramEvent;
import org.eclipse.sirius.components.diagrams.events.MoveEvent;
import org.eclipse.sirius.components.diagrams.events.ResizeEvent;
import org.eclipse.sirius.components.diagrams.layout.ISiriusWebLayoutConfigurator;
import org.eclipse.sirius.components.diagrams.layout.api.Bounds;
import org.eclipse.sirius.components.diagrams.layout.api.Geometry;
import org.eclipse.sirius.components.diagrams.layout.api.PointOnRectangleInfo;
import org.eclipse.sirius.components.diagrams.layout.api.RectangleSide;
import org.eclipse.sirius.components.diagrams.layout.incremental.data.NodeLayoutData;
import org.eclipse.sirius.components.diagrams.layout.incremental.provider.BorderNodeLabelPositionProvider;
import org.eclipse.sirius.components.diagrams.layout.incremental.provider.NodePositionProvider;
import org.eclipse.sirius.components.diagrams.layout.incremental.provider.NodeSizeProvider;
import org.springframework.stereotype.Service;
/**
* The engine used to layout border nodes.
*
* @author gcoutable
*/
@Service
public class BorderNodeLayoutEngine implements IBorderNodeLayoutEngine {
private final NodePositionProvider nodePositionProvider;
private final NodeSizeProvider nodeSizeProvider;
private final BorderNodeLabelPositionProvider borderNodeLabelPositionProvider;
public BorderNodeLayoutEngine(NodeSizeProvider nodeSizeProvider) {
this.nodeSizeProvider = Objects.requireNonNull(nodeSizeProvider);
this.borderNodeLabelPositionProvider = new BorderNodeLabelPositionProvider();
this.nodePositionProvider = new NodePositionProvider();
}
@Override
public List<BorderNodesOnSide> layoutBorderNodes(Optional<IDiagramEvent> optionalDiagramElementEvent, List<NodeLayoutData> borderNodesLayoutData, Bounds initialNodeBounds, Bounds newNodeBounds,
ISiriusWebLayoutConfigurator layoutConfigurator) {
List<BorderNodesOnSide> borderNodesPerSide = new ArrayList<>();
if (!borderNodesLayoutData.isEmpty()) {
for (NodeLayoutData nodeLayoutData : borderNodesLayoutData) {
// 1- update the position of the border node if it has been explicitly moved
this.updateBorderNodePosition(optionalDiagramElementEvent, nodeLayoutData);
Size size = this.nodeSizeProvider.getSize(optionalDiagramElementEvent, nodeLayoutData, layoutConfigurator);
if (!this.getRoundedSize(size).equals(this.getRoundedSize(nodeLayoutData.getSize()))) {
nodeLayoutData.setSize(size);
nodeLayoutData.setChanged(true);
}
}
// 2- recompute the border node
borderNodesPerSide = this.snapBorderNodes(borderNodesLayoutData, initialNodeBounds.getSize(), layoutConfigurator);
// 3 - move the border node along the side according to the side change
this.updateBorderNodeAccordingParentResize(optionalDiagramElementEvent, initialNodeBounds, newNodeBounds, borderNodesPerSide, borderNodesLayoutData.get(0).getParent().getId());
// 4- set the label position if the border is newly created
this.updateBorderNodeLabel(optionalDiagramElementEvent, borderNodesPerSide);
}
return borderNodesPerSide;
}
private void updateBorderNodeLabel(Optional<IDiagramEvent> optionalDiagramElementEvent, List<BorderNodesOnSide> borderNodesPerSideList) {
for (BorderNodesOnSide borderNodesOnSide : borderNodesPerSideList) {
RectangleSide side = borderNodesOnSide.getSide();
List<NodeLayoutData> borderNodes = borderNodesOnSide.getBorderNodes();
for (NodeLayoutData borderNodeLayoutData : borderNodes) {
this.borderNodeLabelPositionProvider.updateLabelPosition(optionalDiagramElementEvent, side, borderNodeLayoutData);
}
}
}
/**
* Move the border node along the side according to the parent Size changes.
*/
private void updateBorderNodeAccordingParentResize(Optional<IDiagramEvent> optionalDiagramElementEvent, Bounds initialNodeBounds, Bounds newNodeBounds,
List<BorderNodesOnSide> borderNodesPerSideList, String parentId) {
// @formatter:off
boolean isParentRectangleResized = optionalDiagramElementEvent
.filter(ResizeEvent.class::isInstance)
.map(ResizeEvent.class::cast)
.map(ResizeEvent::getNodeId)
.filter(parentId::equals)
.isPresent();
// @formatter:on
isParentRectangleResized = isParentRectangleResized || !initialNodeBounds.getSize().equals(newNodeBounds.getSize());
if (isParentRectangleResized && !initialNodeBounds.equals(newNodeBounds)) {
EnumMap<RectangleSide, Double> sideHomotheticRatio = this.getHomotheticRatio(initialNodeBounds.getSize(), newNodeBounds.getSize());
Size initialSize = initialNodeBounds.getSize();
Size newSize = newNodeBounds.getSize();
for (BorderNodesOnSide borderNodesOnSide : borderNodesPerSideList) {
RectangleSide side = borderNodesOnSide.getSide();
List<NodeLayoutData> borderNodes = borderNodesOnSide.getBorderNodes();
double homotheticRatio = sideHomotheticRatio.get(side);
for (NodeLayoutData borderNodeLayoutData : borderNodes) {
// The border node position is done in the parent node coordinate system
Position position = borderNodeLayoutData.getPosition();
Size size = borderNodeLayoutData.getSize();
if (RectangleSide.NORTH.equals(side)) {
borderNodeLayoutData.setPosition(Position.at((position.getX() + size.getWidth() / 2) * homotheticRatio - size.getWidth() / 2, position.getY()));
} else if (RectangleSide.SOUTH.equals(side)) {
double dySouthShift = newSize.getHeight() - initialSize.getHeight();
borderNodeLayoutData.setPosition(Position.at((position.getX() + size.getWidth() / 2) * homotheticRatio - size.getWidth() / 2, position.getY() + dySouthShift));
} else if (RectangleSide.WEST.equals(side)) {
borderNodeLayoutData.setPosition(Position.at(position.getX(), (position.getY() + size.getHeight() / 2) * homotheticRatio - size.getHeight() / 2));
} else if (RectangleSide.EAST.equals(side)) {
double dxEastShift = newSize.getWidth() - initialSize.getWidth();
borderNodeLayoutData.setPosition(Position.at(position.getX() + dxEastShift, (position.getY() + size.getHeight() / 2) * homotheticRatio - size.getHeight() / 2));
}
}
}
}
}
private void updateBorderNodePosition(Optional<IDiagramEvent> optionalDiagramElementEvent, NodeLayoutData nodeLayoutData) {
// @formatter:off
optionalDiagramElementEvent.filter(MoveEvent.class::isInstance)
.map(MoveEvent.class::cast)
.map(MoveEvent::getNodeId)
.filter(nodeLayoutData.getId()::equals)
.ifPresent(nodeId -> {
Position position = this.nodePositionProvider.getPosition(optionalDiagramElementEvent, nodeLayoutData);
if (!position.equals(nodeLayoutData.getPosition())) {
nodeLayoutData.setPosition(position);
nodeLayoutData.setChanged(true);
nodeLayoutData.setPinned(true);
}
});
// @formatter:on
}
/**
* Update the border node by snapping it to the parentRectangle, that is moving it to the closest point of the
* parentRectangle.
*
* @param borderNodesLayoutData
* the border nodes which position is given in the rectangle upper right corner coordinates system
* @return for each side of the given parentRectangle, the list of the updates border node
*/
private List<BorderNodesOnSide> snapBorderNodes(List<NodeLayoutData> borderNodesLayoutData, Size parentRectangle, ISiriusWebLayoutConfigurator layoutConfigurator) {
EnumMap<RectangleSide, List<NodeLayoutData>> borderNodesPerSide = new EnumMap<>(RectangleSide.class);
Geometry geometry = new Geometry();
for (NodeLayoutData borderNodeLayoutData : borderNodesLayoutData) {
double portOffset = layoutConfigurator.configureByType(borderNodeLayoutData.getNodeType()).getProperty(CoreOptions.PORT_BORDER_OFFSET).doubleValue();
Bounds borderNodeRectangle = Bounds.newBounds().position(borderNodeLayoutData.getPosition()).size(borderNodeLayoutData.getSize()).build();
PointOnRectangleInfo borderNodePositionOnSide = geometry.snapBorderNodeOnRectangle(borderNodeRectangle, parentRectangle, portOffset);
// update the border node
borderNodeLayoutData.setPosition(borderNodePositionOnSide.getPosition());
borderNodesPerSide.computeIfAbsent(borderNodePositionOnSide.getSide(), side -> new ArrayList<>());
borderNodesPerSide.get(borderNodePositionOnSide.getSide()).add(borderNodeLayoutData);
}
// @formatter:off
return borderNodesPerSide.entrySet().stream()
.map(entry -> new BorderNodesOnSide(entry.getKey(), entry.getValue()))
.toList();
// @formatter:on
}
/**
* This method compares two rectangles and for each side return the homothetic ratio.
*/
private EnumMap<RectangleSide, Double> getHomotheticRatio(Size rectangle1, Size rectangle2) {
EnumMap<RectangleSide, Double> sideToHomotheticRatio = new EnumMap<>(RectangleSide.class);
double initialHeight = rectangle1.getHeight();
double initialWidth = rectangle1.getWidth();
double newHeight = rectangle2.getHeight();
double newWidth = rectangle2.getWidth();
double verticalRatio = 0;
if (initialHeight != 0) {
verticalRatio = newHeight / initialHeight;
}
double horizontalRatio = 0;
if (initialWidth != 0) {
horizontalRatio = newWidth / initialWidth;
}
sideToHomotheticRatio.put(RectangleSide.NORTH, horizontalRatio);
sideToHomotheticRatio.put(RectangleSide.SOUTH, horizontalRatio);
sideToHomotheticRatio.put(RectangleSide.EAST, verticalRatio);
sideToHomotheticRatio.put(RectangleSide.WEST, verticalRatio);
return sideToHomotheticRatio;
}
/**
* Round size to 1/1000 of a pixel. It is needed when an image has a width or height with a very big decimal part
* (e.g: 140.0004672837)
*
* @param size
* the {@link Size} to round.
* @return the rounded size.
*/
private Size getRoundedSize(Size size) {
BigDecimal roundedWidth = BigDecimal.valueOf(size.getWidth()).setScale(4, RoundingMode.HALF_UP);
BigDecimal roundedHeight = BigDecimal.valueOf(size.getHeight()).setScale(4, RoundingMode.HALF_UP);
return Size.of(roundedWidth.doubleValue(), roundedHeight.doubleValue());
}
}