NIFI-4839 - Implemented auto-layout when importing the PG. Will find an available spot on a canvas which doesn't overlap with other components and is as close to the canvas center as possible.

This commit is contained in:
Andrew Grande 2018-02-05 17:33:28 -05:00 committed by Pierre Villard
parent 9c3594ded6
commit e3cc7bee05
5 changed files with 189 additions and 4 deletions

View File

@ -84,5 +84,9 @@
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -16,6 +16,7 @@
*/
package org.apache.nifi.toolkit.cli.impl.client.nifi;
import org.apache.nifi.toolkit.cli.impl.client.nifi.impl.PgBox;
import org.apache.nifi.web.api.entity.CurrentUserEntity;
import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
@ -48,6 +49,16 @@ public interface FlowClient {
*/
ProcessGroupFlowEntity getProcessGroup(String id) throws NiFiClientException, IOException;
/**
* Suggest a location for the new process group on a canvas, within a given process group.
* Will locate to the right and then bottom and offset for the size of the PG box.
* @param parentId
* @return
* @throws NiFiClientException
* @throws IOException
*/
PgBox getSuggestedProcessGroupCoordinates(String parentId) throws NiFiClientException, IOException;
/**
* Schedules the components of a process group.
*

View File

@ -16,6 +16,8 @@
*/
package org.apache.nifi.toolkit.cli.impl.client.nifi.impl;
import com.jayway.jsonpath.JsonPath;
import net.minidev.json.JSONArray;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
@ -27,9 +29,13 @@ import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataSetEntity;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Jersey implementation of FlowClient.
@ -78,6 +84,41 @@ public class JerseyFlowClient extends AbstractJerseyClient implements FlowClient
});
}
@Override
public PgBox getSuggestedProcessGroupCoordinates(String parentId) throws NiFiClientException, IOException {
if (StringUtils.isBlank(parentId)) {
throw new IllegalArgumentException("Process group id cannot be null");
}
return executeAction("Error retrieving process group flow", () -> {
final WebTarget target = flowTarget
.path("process-groups/{id}")
.resolveTemplate("id", parentId);
Response response = getRequestBuilder(target).get();
String json = response.readEntity(String.class);
JSONArray jsonArray = JsonPath.compile("$..position").read(json);
if (jsonArray.isEmpty()) {
// it's an empty nifi canvas, nice to align
return PgBox.CANVAS_CENTER;
}
List<PgBox> coords = new HashSet<>(jsonArray) // de-dup the initial set
.stream().map(Map.class::cast)
.map(m -> new PgBox(Double.valueOf(m.get("x").toString()).intValue(),
Double.valueOf(m.get("y").toString()).intValue()))
.collect(Collectors.toList());
PgBox freeSpot = coords.get(0).findFreeSpace(coords);
return freeSpot;
});
}
@Override
public ScheduleComponentsEntity scheduleProcessGroupComponents(
final String processGroupId, final ScheduleComponentsEntity scheduleComponentsEntity)

View File

@ -0,0 +1,124 @@
package org.apache.nifi.toolkit.cli.impl.client.nifi.impl;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Represents the bounding box the Processing Group and some placement logic.
*/
public class PgBox implements Comparable<PgBox> {
// values as specified in the nf-process-group.js file
public static final int PG_SIZE_WIDTH = 380;
public static final int PG_SIZE_HEIGHT = 172;
// minimum whitespace between PG elements for auto-layout
public static final int PG_SPACING = 50;
public int x;
public int y;
public static final PgBox CANVAS_CENTER = new PgBox(0, 0);
public PgBox(int x, int y) {
this.x = x;
this.y = y;
}
/**
* @return distance from a (0, 0) point.
*/
public int distance() {
// a simplified distance formula because the other coord is (0, 0)
return (int) Math.hypot(x, y);
}
public boolean intersects(PgBox other) {
// adapted for java.awt Rectangle, we don't want to import it
// assume everything to be of the PG size for simplicity
int tw = PG_SIZE_WIDTH;
int th = PG_SIZE_HEIGHT;
// 2nd pg box includes spacers
int rw = PG_SIZE_WIDTH;
int rh = PG_SIZE_HEIGHT;
if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) {
return false;
}
double tx = this.x;
double ty = this.y;
double rx = other.x;
double ry = other.y;
rw += rx;
rh += ry;
tw += tx;
th += ty;
// overflow || intersect
return ((rw < rx || rw > tx) &&
(rh < ry || rh > ty) &&
(tw < tx || tw > rx) &&
(th < ty || th > ry));
}
public PgBox findFreeSpace(List<PgBox> allCoords) {
// sort by distance to (0.0)
List<PgBox> byClosest = allCoords.stream().sorted().collect(Collectors.toList());
// search to the right
List<PgBox> freeSpots = byClosest.stream().filter(other ->
byClosest.stream().noneMatch(other.right()::intersects)
).map(PgBox::right).collect(Collectors.toList()); // save a 'transformed' spot 'to the right'
// search down
freeSpots.addAll(byClosest.stream().filter(other ->
byClosest.stream().noneMatch(other.down()::intersects)
).map(PgBox::down).collect(Collectors.toList()));
// search left
freeSpots.addAll(byClosest.stream().filter(other ->
byClosest.stream().noneMatch(other.left()::intersects)
).map(PgBox::left).collect(Collectors.toList()));
// search above
freeSpots.addAll(byClosest.stream().filter(other ->
byClosest.stream().noneMatch(other.up()::intersects)
).map(PgBox::up).collect(Collectors.toList()));
// return a free spot closest to (0, 0)
return freeSpots.stream().sorted().findFirst().orElse(CANVAS_CENTER);
}
public PgBox right() {
return new PgBox(this.x + PG_SIZE_WIDTH + PG_SPACING, this.y);
}
public PgBox down() {
return new PgBox(this.x, this.y + PG_SIZE_HEIGHT + PG_SPACING);
}
public PgBox up() {
return new PgBox(this.x, this.y - PG_SPACING - PG_SIZE_HEIGHT);
}
public PgBox left() {
return new PgBox(this.x - PG_SPACING - PG_SIZE_WIDTH, this.y);
}
@Override
public int compareTo(PgBox other) {
return this.distance() - other.distance();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PgBox pgBox = (PgBox) o;
return Double.compare(pgBox.x, x) == 0 &&
Double.compare(pgBox.y, y) == 0;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}

View File

@ -23,6 +23,7 @@ import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClient;
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
import org.apache.nifi.toolkit.cli.impl.client.nifi.ProcessGroupClient;
import org.apache.nifi.toolkit.cli.impl.client.nifi.impl.PgBox;
import org.apache.nifi.toolkit.cli.impl.command.CommandOption;
import org.apache.nifi.toolkit.cli.impl.command.nifi.AbstractNiFiCommand;
import org.apache.nifi.web.api.dto.PositionDTO;
@ -62,14 +63,15 @@ public class PGImport extends AbstractNiFiCommand {
final String flowId = getRequiredArg(properties, CommandOption.FLOW_ID);
final Integer flowVersion = getRequiredIntArg(properties, CommandOption.FLOW_VERSION);
// TODO - do we actually want the client to deal with X/Y coordinates? drop the args from API?
Integer posX = getIntArg(properties, CommandOption.POS_X);
if (posX == null) {
posX = new Integer(0);
posX = 0;
}
Integer posY = getIntArg(properties, CommandOption.POS_Y);
if (posY == null) {
posY = new Integer(0);
posY = 0;
}
// get the optional id of the parent PG, otherwise fallback to the root group
@ -85,12 +87,15 @@ public class PGImport extends AbstractNiFiCommand {
versionControlInfo.setFlowId(flowId);
versionControlInfo.setVersion(flowVersion);
PgBox pgBox = client.getFlowClient().getSuggestedProcessGroupCoordinates(parentPgId);
final PositionDTO posDto = new PositionDTO();
posDto.setX(posX.doubleValue());
posDto.setY(posY.doubleValue());
posDto.setX(Integer.valueOf(pgBox.x).doubleValue());
posDto.setY(Integer.valueOf(pgBox.y).doubleValue());
final ProcessGroupDTO pgDto = new ProcessGroupDTO();
pgDto.setVersionControlInformation(versionControlInfo);
pgDto.setPosition(posDto);
final ProcessGroupEntity pgEntity = new ProcessGroupEntity();
pgEntity.setComponent(pgDto);