From e3cc7bee057e7cbc3c7c852f170d5fa34749cdb5 Mon Sep 17 00:00:00 2001 From: Andrew Grande Date: Mon, 5 Feb 2018 17:33:28 -0500 Subject: [PATCH] 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. --- nifi-toolkit/nifi-toolkit-cli/pom.xml | 4 + .../cli/impl/client/nifi/FlowClient.java | 11 ++ .../client/nifi/impl/JerseyFlowClient.java | 41 ++++++ .../cli/impl/client/nifi/impl/PgBox.java | 124 ++++++++++++++++++ .../cli/impl/command/nifi/pg/PGImport.java | 13 +- 5 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/PgBox.java diff --git a/nifi-toolkit/nifi-toolkit-cli/pom.xml b/nifi-toolkit/nifi-toolkit-cli/pom.xml index 2f9e18c684..448e85b695 100644 --- a/nifi-toolkit/nifi-toolkit-cli/pom.xml +++ b/nifi-toolkit/nifi-toolkit-cli/pom.xml @@ -84,5 +84,9 @@ commons-io 2.6 + + com.jayway.jsonpath + json-path + diff --git a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/FlowClient.java b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/FlowClient.java index c8cbbdec8b..b27f0ad1b2 100644 --- a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/FlowClient.java +++ b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/FlowClient.java @@ -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. * diff --git a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java index bfbad0c649..f762fabc2d 100644 --- a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java +++ b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java @@ -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 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) diff --git a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/PgBox.java b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/PgBox.java new file mode 100644 index 0000000000..5c4f575588 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/PgBox.java @@ -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 { + // 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 allCoords) { + // sort by distance to (0.0) + List byClosest = allCoords.stream().sorted().collect(Collectors.toList()); + + // search to the right + List 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); + } +} diff --git a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/pg/PGImport.java b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/pg/PGImport.java index e10fb71d3b..77958c8faa 100644 --- a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/pg/PGImport.java +++ b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/pg/PGImport.java @@ -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);