You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2021/05/24 21:19:13 UTC

[incubator-streampipes] 02/02: [STREAMPIPES-375] Persist position of pipeline elements on pipeline canvas

This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git

commit bd07903ab31a033934c118b7b13c472429c31fe4
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Mon May 24 23:18:58 2021 +0200

    [STREAMPIPES-375] Persist position of pipeline elements on pipeline canvas
---
 .../backend/StreamPipesResourceConfig.java         |   2 +
 .../streampipes/model/canvas/CanvasPosition.java   |  43 +++++++++
 .../model/canvas/PipelineCanvasComment.java        |  43 +++++++++
 .../model/canvas/PipelineCanvasMetadata.java       |  82 ++++++++++++++++
 .../model/canvas/PipelineElementMetadata.java      |  43 +++++++++
 .../rest/impl/AbstractRestResource.java            |  10 --
 .../rest/impl/PipelineCanvasMetadataCache.java     |  54 +++++++++++
 .../rest/impl/PipelineCanvasMetadataResource.java  | 106 +++++++++++++++++++++
 .../streampipes/storage/api/INoSqlStorage.java     |   2 +
 .../api/IPipelineCanvasMetadataStorage.java        |  25 +++++
 .../storage/couchdb/CouchDbStorageManager.java     |   5 +-
 .../impl/PipelineCanvasMetadataStorageImpl.java    |  69 ++++++++++++++
 .../streampipes/storage/couchdb/utils/Utils.java   |   4 +
 ui/src/app/core-model/gen/streampipes-model.ts     |  75 ++++++++++++++-
 .../pipeline-assembly.component.html               |   1 +
 .../pipeline-assembly.component.ts                 |  60 +++++++++---
 .../components/pipeline/pipeline.component.ts      |  48 +++++-----
 .../save-pipeline/save-pipeline.component.ts       |  30 +++++-
 ui/src/app/editor/services/editor.service.ts       |  23 ++++-
 .../services/pipeline-positioning.service.ts       |  76 ++++++++++++---
 .../apis/pipeline-canvas-metadata.service.ts       |  65 +++++++++++++
 ui/src/app/platform-services/platform.module.ts    |   2 +
 22 files changed, 794 insertions(+), 74 deletions(-)

diff --git a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
index e6d9521..f2fd784 100644
--- a/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
+++ b/streampipes-backend/src/main/java/org/apache/streampipes/backend/StreamPipesResourceConfig.java
@@ -70,6 +70,8 @@ public class StreamPipesResourceConfig extends ResourceConfig {
     register(OntologyKnowledge.class);
     register(OntologyMeasurementUnit.class);
     register(OntologyPipelineElement.class);
+    register(PipelineCanvasMetadataCache.class);
+    register(PipelineCanvasMetadataResource.class);
     register(PipelineCache.class);
     register(PipelineCategory.class);
     register(PipelineElementAsset.class);
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/CanvasPosition.java b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/CanvasPosition.java
new file mode 100644
index 0000000..bb94bf2
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/CanvasPosition.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.model.canvas;
+
+public class CanvasPosition {
+
+  private float x;
+  private float y;
+
+  public CanvasPosition() {
+  }
+
+  public float getX() {
+    return x;
+  }
+
+  public void setX(float x) {
+    this.x = x;
+  }
+
+  public float getY() {
+    return y;
+  }
+
+  public void setY(float y) {
+    this.y = y;
+  }
+}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineCanvasComment.java b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineCanvasComment.java
new file mode 100644
index 0000000..620e0b6
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineCanvasComment.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.model.canvas;
+
+public class PipelineCanvasComment {
+
+  private String comment;
+  private CanvasPosition position;
+
+  public PipelineCanvasComment() {
+  }
+
+  public String getComment() {
+    return comment;
+  }
+
+  public void setComment(String comment) {
+    this.comment = comment;
+  }
+
+  public CanvasPosition getPosition() {
+    return position;
+  }
+
+  public void setPosition(CanvasPosition position) {
+    this.position = position;
+  }
+}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineCanvasMetadata.java b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineCanvasMetadata.java
new file mode 100644
index 0000000..d22c9be
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineCanvasMetadata.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.model.canvas;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+import org.apache.streampipes.model.shared.annotation.TsModel;
+
+import java.util.List;
+import java.util.Map;
+
+@TsModel
+public class PipelineCanvasMetadata {
+
+  @JsonProperty("_id")
+  private @SerializedName("_id") String id;
+
+  @JsonProperty("_rev")
+  private @SerializedName("_rev") String rev;
+
+  private String pipelineId;
+  private Map<String, PipelineElementMetadata> pipelineElementMetadata;
+  private List<PipelineCanvasComment> comments;
+
+  public PipelineCanvasMetadata() {
+  }
+
+  public String getPipelineId() {
+    return pipelineId;
+  }
+
+  public void setPipelineId(String pipelineId) {
+    this.pipelineId = pipelineId;
+  }
+
+  public Map<String, PipelineElementMetadata> getPipelineElementMetadata() {
+    return pipelineElementMetadata;
+  }
+
+  public void setPipelineElementMetadata(Map<String, PipelineElementMetadata> pipelineElementMetadata) {
+    this.pipelineElementMetadata = pipelineElementMetadata;
+  }
+
+  public List<PipelineCanvasComment> getComments() {
+    return comments;
+  }
+
+  public void setComments(List<PipelineCanvasComment> comments) {
+    this.comments = comments;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getRev() {
+    return rev;
+  }
+
+  public void setRev(String rev) {
+    this.rev = rev;
+  }
+}
diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineElementMetadata.java b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineElementMetadata.java
new file mode 100644
index 0000000..42012d6
--- /dev/null
+++ b/streampipes-model/src/main/java/org/apache/streampipes/model/canvas/PipelineElementMetadata.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.model.canvas;
+
+public class PipelineElementMetadata {
+
+  private CanvasPosition position;
+  private String customName;
+
+  public PipelineElementMetadata() {
+  }
+
+  public CanvasPosition getPosition() {
+    return position;
+  }
+
+  public void setPosition(CanvasPosition position) {
+    this.position = position;
+  }
+
+  public String getCustomName() {
+    return customName;
+  }
+
+  public void setCustomName(String customName) {
+    this.customName = customName;
+  }
+}
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AbstractRestResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AbstractRestResource.java
index a40ed88..6680401 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AbstractRestResource.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/AbstractRestResource.java
@@ -59,16 +59,6 @@ public abstract class AbstractRestResource extends AbstractSharedRestInterface {
     }
   }
 
-  protected <T> String toJsonLd(String rootElementUri, T object) {
-    try {
-      return JsonLdUtils.asString(new JsonLdTransformer(rootElementUri).toJsonLd(object));
-    } catch (IllegalAccessException | InvocationTargetException | InvalidRdfException | ClassNotFoundException e) {
-      return toJson(constructErrorMessage(new Notification(NotificationType.UNKNOWN_ERROR.title(),
-              NotificationType.UNKNOWN_ERROR.description(),
-              e.getMessage())));
-    }
-  }
-
   protected IPipelineElementDescriptionStorageCache getPipelineElementRdfStorage() {
     return StorageManager.INSTANCE.getPipelineElementStorage();
   }
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataCache.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataCache.java
new file mode 100644
index 0000000..83e8b52
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataCache.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.rest.impl;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Path("/v2/users/{username}/pipeline-canvas-cache")
+public class PipelineCanvasMetadataCache extends AbstractRestResource {
+
+  private static ConcurrentHashMap<String, String> cachedCanvasMetadata = new ConcurrentHashMap<>();
+
+  @POST
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response updateCachedCanvasMetadata(@PathParam("username") String user,
+                                             String canvasMetadata) {
+    cachedCanvasMetadata.put(user, canvasMetadata);
+    return ok();
+  }
+
+  @GET
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response getCachedCanvasMetadata(@PathParam("username") String user) {
+    if (cachedCanvasMetadata.containsKey(user)) {
+      return ok(cachedCanvasMetadata.get(user));
+    } else {
+      return ok();
+    }
+  }
+
+  @DELETE
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response removeCanvasMetadataFromCache(@PathParam("username") String user) {
+    cachedCanvasMetadata.remove(user);
+    return ok();
+  }
+}
diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataResource.java
new file mode 100644
index 0000000..0aaaa00
--- /dev/null
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/PipelineCanvasMetadataResource.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.rest.impl;
+
+import org.apache.streampipes.model.canvas.PipelineCanvasMetadata;
+import org.apache.streampipes.rest.shared.annotation.JacksonSerialized;
+import org.apache.streampipes.storage.api.IPipelineCanvasMetadataStorage;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/v2/users/{username}/pipeline-canvas-metadata")
+public class PipelineCanvasMetadataResource extends AbstractRestResource {
+
+  @GET
+  @Path("/pipeline/{pipelineId}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @JacksonSerialized
+  public Response getPipelineCanvasMetadataForPipeline(@PathParam("pipelineId") String pipelineId) {
+    try {
+      return ok(getPipelineCanvasMetadataStorage()
+              .getPipelineCanvasMetadataForPipeline(pipelineId));
+    } catch (IllegalArgumentException e) {
+      return badRequest();
+    }
+  }
+
+  @GET
+  @Path("{canvasId}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @JacksonSerialized
+  public Response getPipelineCanvasMetadata(@PathParam("canvasId") String pipelineCanvasId) {
+    try {
+      return ok(getPipelineCanvasMetadataStorage()
+              .getElementById(pipelineCanvasId));
+    } catch (IllegalArgumentException e) {
+      return badRequest();
+    }
+  }
+
+  @POST
+  @Produces(MediaType.APPLICATION_JSON)
+  @JacksonSerialized
+  public Response storePipelineCanvasMetadata(PipelineCanvasMetadata pipelineCanvasMetadata) {
+    getPipelineCanvasMetadataStorage().createElement(pipelineCanvasMetadata);
+    return ok();
+  }
+
+  @DELETE
+  @Produces(MediaType.APPLICATION_JSON)
+  @Path("{canvasId}")
+  @JacksonSerialized
+  public Response deletePipelineCanvasMetadata(@PathParam("canvasId") String pipelineCanvasId) {
+    PipelineCanvasMetadata metadata = find(pipelineCanvasId);
+    getPipelineCanvasMetadataStorage().deleteElement(metadata);
+    return ok();
+  }
+
+  @DELETE
+  @Produces(MediaType.APPLICATION_JSON)
+  @Path("/pipeline/{pipelineId}")
+  @JacksonSerialized
+  public Response deletePipelineCanvasMetadataForPipeline(@PathParam("pipelineId") String pipelineId) {
+    PipelineCanvasMetadata metadata = getPipelineCanvasMetadataStorage().getPipelineCanvasMetadataForPipeline(pipelineId);
+    getPipelineCanvasMetadataStorage().deleteElement(metadata);
+    return ok();
+  }
+
+  @PUT
+  @Produces(MediaType.APPLICATION_JSON)
+  @Path("{canvasId}")
+  @JacksonSerialized
+  public Response updatePipelineCanvasMetadata(@PathParam("canvasId") String pipelineCanvasId,
+                                               PipelineCanvasMetadata pipelineCanvasMetadata) {
+    try {
+      getPipelineCanvasMetadataStorage().updateElement(pipelineCanvasMetadata);
+    } catch (IllegalArgumentException e) {
+      getPipelineCanvasMetadataStorage().createElement(pipelineCanvasMetadata);
+    }
+    return ok();
+  }
+
+  private PipelineCanvasMetadata find(String canvasId) {
+    return getPipelineCanvasMetadataStorage().getElementById(canvasId);
+  }
+
+  private IPipelineCanvasMetadataStorage getPipelineCanvasMetadataStorage() {
+    return getNoSqlStorage().getPipelineCanvasMetadataStorage();
+  }
+}
diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java
index 4f1c176..806735b 100644
--- a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java
+++ b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/INoSqlStorage.java
@@ -59,4 +59,6 @@ public interface INoSqlStorage {
 
   IPipelineElementTemplateStorage getPipelineElementTemplateStorage();
 
+  IPipelineCanvasMetadataStorage getPipelineCanvasMetadataStorage();
+
 }
diff --git a/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPipelineCanvasMetadataStorage.java b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPipelineCanvasMetadataStorage.java
new file mode 100644
index 0000000..fa7c647
--- /dev/null
+++ b/streampipes-storage-api/src/main/java/org/apache/streampipes/storage/api/IPipelineCanvasMetadataStorage.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.storage.api;
+
+import org.apache.streampipes.model.canvas.PipelineCanvasMetadata;
+
+public interface IPipelineCanvasMetadataStorage extends CRUDStorage<String, PipelineCanvasMetadata> {
+
+  PipelineCanvasMetadata getPipelineCanvasMetadataForPipeline(String pipelineId);
+}
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java
index 1bcd490..dce977f 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/CouchDbStorageManager.java
@@ -120,5 +120,8 @@ public enum CouchDbStorageManager implements INoSqlStorage {
     return new PipelineElementTemplateStorageImpl();
   }
 
-
+  @Override
+  public IPipelineCanvasMetadataStorage getPipelineCanvasMetadataStorage() {
+    return new PipelineCanvasMetadataStorageImpl();
+  }
 }
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/PipelineCanvasMetadataStorageImpl.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/PipelineCanvasMetadataStorageImpl.java
new file mode 100644
index 0000000..be9f77d
--- /dev/null
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/impl/PipelineCanvasMetadataStorageImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.storage.couchdb.impl;
+
+import org.apache.streampipes.model.canvas.PipelineCanvasMetadata;
+import org.apache.streampipes.storage.api.IPipelineCanvasMetadataStorage;
+import org.apache.streampipes.storage.couchdb.dao.AbstractDao;
+import org.apache.streampipes.storage.couchdb.utils.Utils;
+
+import java.util.List;
+
+public class PipelineCanvasMetadataStorageImpl extends AbstractDao<PipelineCanvasMetadata>
+        implements IPipelineCanvasMetadataStorage {
+
+  public PipelineCanvasMetadataStorageImpl() {
+    super(Utils::getCouchDbPipelineCanvasMetadataClient, PipelineCanvasMetadata.class);
+  }
+
+  @Override
+  public List<PipelineCanvasMetadata> getAll() {
+    return findAll();
+  }
+
+  @Override
+  public void createElement(PipelineCanvasMetadata element) {
+    persist(element);
+  }
+
+  @Override
+  public PipelineCanvasMetadata getElementById(String id) {
+    return find(id).orElseThrow(IllegalArgumentException::new);
+  }
+
+  @Override
+  public PipelineCanvasMetadata updateElement(PipelineCanvasMetadata element) {
+    update(element);
+    return find(element.getId()).orElseThrow(IllegalAccessError::new);
+  }
+
+  @Override
+  public void deleteElement(PipelineCanvasMetadata element) {
+    delete(element.getId());
+  }
+
+  @Override
+  public PipelineCanvasMetadata getPipelineCanvasMetadataForPipeline(String pipelineId) {
+    // TODO add CouchDB view
+    return findAll()
+            .stream()
+            .filter(p -> p.getPipelineId().equals(pipelineId))
+            .findFirst()
+            .orElseThrow(IllegalArgumentException::new);
+  }
+}
diff --git a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
index f81a084..2679d0d 100644
--- a/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
+++ b/streampipes-storage-couchdb/src/main/java/org/apache/streampipes/storage/couchdb/utils/Utils.java
@@ -28,6 +28,10 @@ public class Utils {
     return getCouchDbGsonClient("pipelineelementtemplate");
   }
 
+  public static CouchDbClient getCouchDbPipelineCanvasMetadataClient() {
+    return getCouchDbGsonClient("pipelinecanvasmetadata");
+  }
+
   public static CouchDbClient getCouchDbCategoryClient() {
     return getCouchDbGsonClient("category");
   }
diff --git a/ui/src/app/core-model/gen/streampipes-model.ts b/ui/src/app/core-model/gen/streampipes-model.ts
index 0fc0532..4fa3957 100644
--- a/ui/src/app/core-model/gen/streampipes-model.ts
+++ b/ui/src/app/core-model/gen/streampipes-model.ts
@@ -16,11 +16,10 @@
  *
  */
 
-
 /* tslint:disable */
 /* eslint-disable */
 // @ts-nocheck
-// Generated using typescript-generator version 2.27.744 on 2021-05-22 23:26:12.
+// Generated using typescript-generator version 2.27.744 on 2021-05-24 18:53:06.
 
 export class AbstractStreamPipesEntity {
     "@class": "org.apache.streampipes.model.base.AbstractStreamPipesEntity" | "org.apache.streampipes.model.base.NamedStreamPipesEntity" | "org.apache.streampipes.model.connect.adapter.AdapterDescription" | "org.apache.streampipes.model.connect.adapter.AdapterSetDescription" | "org.apache.streampipes.model.connect.adapter.GenericAdapterSetDescription" | "org.apache.streampipes.model.connect.adapter.SpecificAdapterSetDescription" | "org.apache.streampipes.model.connect.adapter.AdapterStre [...]
@@ -152,8 +151,8 @@ export class NamedStreamPipesEntity extends AbstractStreamPipesEntity {
         instance.applicationLinks = __getCopyArrayFn(ApplicationLink.fromData)(data.applicationLinks);
         instance.internallyManaged = data.internallyManaged;
         instance.connectedTo = __getCopyArrayFn(__identity<string>())(data.connectedTo);
-        instance.uri = data.uri;
         instance.dom = data.dom;
+        instance.uri = data.uri;
         return instance;
     }
 }
@@ -190,9 +189,9 @@ export class AdapterDescription extends NamedStreamPipesEntity {
         instance.rules = __getCopyArrayFn(TransformationRuleDescription.fromDataUnion)(data.rules);
         instance.category = __getCopyArrayFn(__identity<string>())(data.category);
         instance.createdAt = data.createdAt;
-        instance.streamRules = __getCopyArrayFn(__identity<any>())(data.streamRules);
-        instance.schemaRules = __getCopyArrayFn(__identity<any>())(data.schemaRules);
         instance.valueRules = __getCopyArrayFn(__identity<any>())(data.valueRules);
+        instance.schemaRules = __getCopyArrayFn(__identity<any>())(data.schemaRules);
+        instance.streamRules = __getCopyArrayFn(__identity<any>())(data.streamRules);
         instance.couchDBId = data.couchDBId;
         instance._rev = data._rev;
         return instance;
@@ -601,6 +600,21 @@ export class ApplicationLink extends UnnamedStreamPipesEntity {
     }
 }
 
+export class CanvasPosition {
+    x: number;
+    y: number;
+
+    static fromData(data: CanvasPosition, target?: CanvasPosition): CanvasPosition {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new CanvasPosition();
+        instance.x = data.x;
+        instance.y = data.y;
+        return instance;
+    }
+}
+
 export class Category {
     _id: string;
     _rev: string;
@@ -2077,6 +2091,42 @@ export class Pipeline extends ElementComposition {
     }
 }
 
+export class PipelineCanvasComment {
+    comment: string;
+    position: CanvasPosition;
+
+    static fromData(data: PipelineCanvasComment, target?: PipelineCanvasComment): PipelineCanvasComment {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelineCanvasComment();
+        instance.comment = data.comment;
+        instance.position = CanvasPosition.fromData(data.position);
+        return instance;
+    }
+}
+
+export class PipelineCanvasMetadata {
+    _id: string;
+    _rev: string;
+    comments: PipelineCanvasComment[];
+    pipelineElementMetadata: { [index: string]: PipelineElementMetadata };
+    pipelineId: string;
+
+    static fromData(data: PipelineCanvasMetadata, target?: PipelineCanvasMetadata): PipelineCanvasMetadata {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelineCanvasMetadata();
+        instance.pipelineId = data.pipelineId;
+        instance.pipelineElementMetadata = __getCopyObjectFn(PipelineElementMetadata.fromData)(data.pipelineElementMetadata);
+        instance.comments = __getCopyArrayFn(PipelineCanvasComment.fromData)(data.comments);
+        instance._id = data._id;
+        instance._rev = data._rev;
+        return instance;
+    }
+}
+
 export class PipelineCategory {
     _id: string;
     _rev: string;
@@ -2096,6 +2146,21 @@ export class PipelineCategory {
     }
 }
 
+export class PipelineElementMetadata {
+    customName: string;
+    position: CanvasPosition;
+
+    static fromData(data: PipelineElementMetadata, target?: PipelineElementMetadata): PipelineElementMetadata {
+        if (!data) {
+            return data;
+        }
+        const instance = target || new PipelineElementMetadata();
+        instance.position = CanvasPosition.fromData(data.position);
+        instance.customName = data.customName;
+        return instance;
+    }
+}
+
 export class PipelineElementMonitoringInfo {
     consumedMessageInfoExists: boolean;
     consumedMessagesInfos: ConsumedMessagesInfo[];
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
index bf42643..6a616a4 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
@@ -145,6 +145,7 @@
                           [allElements]="allElements"
                           [preview]="false"
                           [pipelineCached]="pipelineCached"
+                          [pipelineCanvasMetadata]="pipelineCanvasMetadata"
                           [pipelineCacheRunning]="pipelineCacheRunning"
                           (pipelineCachedChanged)="pipelineCached=$event"
                           (pipelineCacheRunningChanged)="pipelineCacheRunning=$event">
diff --git a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
index e78ee18..ed3c629 100644
--- a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
+++ b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
@@ -40,11 +40,13 @@ import {ConfirmDialogComponent} from "../../../core-ui/dialog/confirm-dialog/con
 import {MatDialog} from "@angular/material/dialog";
 import {EditorService} from "../../services/editor.service";
 import {PipelineService} from "../../../platform-services/apis/pipeline.service";
-import {PipelineCanvasScrollingService} from "../../services/pipeline-canvas-scrolling.service";
 import {JsplumbFactoryService} from "../../services/jsplumb-factory.service";
 import Panzoom, {PanzoomObject} from "@panzoom/panzoom";
 import {PipelineElementDraggedService} from "../../services/pipeline-element-dragged.service";
 import {PipelineComponent} from "../pipeline/pipeline.component";
+import {PipelineCanvasMetadata} from "../../../core-model/gen/streampipes-model";
+import {forkJoin} from 'rxjs';
+import {PipelineCanvasMetadataService} from "../../../platform-services/apis/pipeline-canvas-metadata.service";
 
 
 @Component({
@@ -85,6 +87,9 @@ export class PipelineAssemblyComponent implements OnInit {
     pipelineCacheRunning: boolean = false;
     pipelineCached: boolean = false;
 
+    pipelineCanvasMetadata: PipelineCanvasMetadata = new PipelineCanvasMetadata();
+    pipelineCanvasMetadataAvailable: boolean = false;
+
     config: any = {};
     @ViewChild("outerCanvas") pipelineCanvas: ElementRef;
 
@@ -104,7 +109,8 @@ export class PipelineAssemblyComponent implements OnInit {
                 private dialogService: DialogService,
                 private dialog: MatDialog,
                 private ngZone: NgZone,
-                private pipelineElementDraggedService: PipelineElementDraggedService) {
+                private pipelineElementDraggedService: PipelineElementDraggedService,
+                private pipelineCanvasMetadataService: PipelineCanvasMetadataService) {
 
         this.selectMode = true;
         this.currentZoomLevel = 1;
@@ -200,7 +206,10 @@ export class PipelineAssemblyComponent implements OnInit {
         this.currentZoomLevel = 1;
         this.JsplumbBridge.setZoom(this.currentZoomLevel);
         this.JsplumbBridge.repaintEverything();
-        this.EditorService.removePipelineFromCache().subscribe(msg => {
+
+        let removePipelineFromCache = this.EditorService.removePipelineFromCache();
+        let removeCanvasMetadataFromCache = this.EditorService.removeCanvasMetadataFromCache();
+        forkJoin([removePipelineFromCache, removeCanvasMetadataFromCache]).subscribe(msg => {
             this.pipelineCached = false;
             this.pipelineCacheRunning = false;
         });
@@ -211,7 +220,7 @@ export class PipelineAssemblyComponent implements OnInit {
      */
     submit() {
         var pipeline = this.ObjectProvider.makeFinalPipeline(this.rawPipelineModel);
-
+        this.PipelinePositioningService.collectPipelineElementPositions(this.pipelineCanvasMetadata, this.rawPipelineModel);
         pipeline.name = this.currentPipelineName;
         pipeline.description = this.currentPipelineDescription;
         if (this.currentModifiedPipelineId) {
@@ -223,34 +232,55 @@ export class PipelineAssemblyComponent implements OnInit {
             title: "Save pipeline",
             data: {
                 "pipeline": pipeline,
-                "currentModifiedPipelineId": this.currentModifiedPipelineId
+                "currentModifiedPipelineId": this.currentModifiedPipelineId,
+                "pipelineCanvasMetadata": this.pipelineCanvasMetadata
             }
         });
     }
 
     checkAndDisplayCachedPipeline() {
-        this.EditorService.getCachedPipeline().subscribe(msg => {
-            if (msg) {
-                this.rawPipelineModel = msg;
-                this.displayPipelineInEditor(true);
+        let cachedPipeline = this.EditorService.getCachedPipeline();
+        let cachedCanvasMetadata = this.EditorService.getCachedPipelineCanvasMetadata();
+        forkJoin([cachedPipeline, cachedCanvasMetadata]).subscribe(results => {
+            if (results[0] && results[0].length > 0) {
+                this.rawPipelineModel = results[0] as PipelineElementConfig[];
+                this.handleCanvasMetadataResponse(results[1]);
+                this.displayPipelineInEditor(!this.pipelineCanvasMetadataAvailable, this.pipelineCanvasMetadata);
             }
         });
     }
 
     displayPipelineById() {
-        this.PipelineService.getPipelineById(this.currentModifiedPipelineId)
-            .subscribe((msg) => {
-                let pipeline = msg;
+        let pipelineRequest = this.PipelineService.getPipelineById(this.currentModifiedPipelineId);
+        let canvasRequest = this.pipelineCanvasMetadataService.getPipelineCanvasMetadata(this.currentModifiedPipelineId);
+        pipelineRequest.subscribe(pipelineResp => {
+                let pipeline = pipelineResp;
                 this.currentPipelineName = pipeline.name;
                 this.currentPipelineDescription = pipeline.description;
                 this.rawPipelineModel = this.JsplumbService.makeRawPipeline(pipeline, false);
-                this.displayPipelineInEditor(true);
+                canvasRequest.subscribe(canvasResp => {
+                    this.handleCanvasMetadataResponse(canvasResp);
+                }, error => {
+                    this.handleCanvasMetadataResponse(undefined);
+                });
             });
     };
 
-    displayPipelineInEditor(autoLayout) {
+    handleCanvasMetadataResponse(canvasMetadata: PipelineCanvasMetadata) {
+        if (canvasMetadata) {
+            this.pipelineCanvasMetadata = canvasMetadata;
+            this.pipelineCanvasMetadataAvailable = true;
+        } else {
+            this.pipelineCanvasMetadataAvailable = false;
+            this.pipelineCanvasMetadata = new PipelineCanvasMetadata();
+        }
+        this.displayPipelineInEditor(!this.pipelineCanvasMetadataAvailable, this.pipelineCanvasMetadata);
+    }
+
+    displayPipelineInEditor(autoLayout,
+                            pipelineCanvasMetadata?: PipelineCanvasMetadata) {
         setTimeout(() => {
-            this.PipelinePositioningService.displayPipeline(this.rawPipelineModel, "#assembly", false, autoLayout);
+            this.PipelinePositioningService.displayPipeline(this.rawPipelineModel, "#assembly", false, autoLayout, pipelineCanvasMetadata);
             this.EditorService.makePipelineAssemblyEmpty(false);
             this.ngZone.run(() => {
                 this.pipelineValid = this.PipelineValidationService
diff --git a/ui/src/app/editor/components/pipeline/pipeline.component.ts b/ui/src/app/editor/components/pipeline/pipeline.component.ts
index c478963..08288fc 100644
--- a/ui/src/app/editor/components/pipeline/pipeline.component.ts
+++ b/ui/src/app/editor/components/pipeline/pipeline.component.ts
@@ -21,15 +21,7 @@ import {JsplumbService} from "../../services/jsplumb.service";
 import {PipelineEditorService} from "../../services/pipeline-editor.service";
 import {JsplumbBridge} from "../../services/jsplumb-bridge.service";
 import {ShepherdService} from "../../../services/tour/shepherd.service";
-import {
-  ChangeDetectorRef,
-  Component, ElementRef,
-  EventEmitter,
-  Input,
-  NgZone, OnDestroy,
-  OnInit,
-  Output, ViewChild
-} from "@angular/core";
+import {Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output} from "@angular/core";
 import {
   InvocablePipelineElementUnion,
   PipelineElementConfig,
@@ -37,9 +29,13 @@ import {
 } from "../../model/editor.model";
 import {
   CustomOutputStrategy,
-  DataProcessorInvocation, DataSinkInvocation, ErrorMessage,
-  Pipeline, PipelinePreviewModel, SpDataSet,
-  SpDataStream, SpDataStreamUnion
+  DataProcessorInvocation,
+  DataSinkInvocation,
+  Pipeline,
+  PipelineCanvasMetadata,
+  PipelinePreviewModel,
+  SpDataSet,
+  SpDataStream
 } from "../../../core-model/gen/streampipes-model";
 import {ObjectProvider} from "../../services/object-provider.service";
 import {CustomizeComponent} from "../../dialog/customize/customize.component";
@@ -51,10 +47,9 @@ import {MatchingErrorComponent} from "../../dialog/matching-error/matching-error
 import {Tuple2} from "../../../core-model/base/Tuple2";
 import {ConfirmDialogComponent} from "../../../core-ui/dialog/confirm-dialog/confirm-dialog.component";
 import {MatDialog} from "@angular/material/dialog";
-import {Subject} from "rxjs";
-import {PipelineElementDraggedService} from "../../services/pipeline-element-dragged.service";
-import {PipelineCanvasScrollingService} from "../../services/pipeline-canvas-scrolling.service";
+import {forkJoin} from "rxjs";
 import {JsplumbFactoryService} from "../../services/jsplumb-factory.service";
+import {PipelinePositioningService} from "../../services/pipeline-positioning.service";
 
 @Component({
   selector: 'pipeline',
@@ -87,6 +82,9 @@ export class PipelineComponent implements OnInit, OnDestroy {
   @Input()
   pipelineCacheRunning: boolean;
 
+  @Input()
+  pipelineCanvasMetadata: PipelineCanvasMetadata;
+
   @Output()
   pipelineCacheRunningChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
 
@@ -109,6 +107,7 @@ export class PipelineComponent implements OnInit, OnDestroy {
 
   constructor(private JsplumbService: JsplumbService,
               private PipelineEditorService: PipelineEditorService,
+              private PipelinePositioningService: PipelinePositioningService,
               private JsplumbFactoryService: JsplumbFactoryService,
               private ObjectProvider: ObjectProvider,
               private EditorService: EditorService,
@@ -392,13 +391,18 @@ export class PipelineComponent implements OnInit, OnDestroy {
   }
 
   triggerPipelineCacheUpdate() {
-    this.pipelineCacheRunning = true;
-    this.pipelineCacheRunningChanged.emit(this.pipelineCacheRunning);
-    this.EditorService.updateCachedPipeline(this.rawPipelineModel).subscribe(msg => {
-      this.pipelineCacheRunning = false;
-      this.pipelineCacheRunningChanged.emit(this.pipelineCacheRunning)
-      this.pipelineCached = true;
-      this.pipelineCachedChanged.emit(this.pipelineCached);
+    setTimeout(() => {
+      this.pipelineCacheRunning = true;
+      this.pipelineCacheRunningChanged.emit(this.pipelineCacheRunning);
+      this.PipelinePositioningService.collectPipelineElementPositions(this.pipelineCanvasMetadata, this.rawPipelineModel);
+      let updateCachedPipeline = this.EditorService.updateCachedPipeline(this.rawPipelineModel);
+      let updateCachedCanvasMetadata = this.EditorService.updateCachedCanvasMetadata(this.pipelineCanvasMetadata);
+      forkJoin([updateCachedPipeline, updateCachedCanvasMetadata]).subscribe(msg => {
+        this.pipelineCacheRunning = false;
+        this.pipelineCacheRunningChanged.emit(this.pipelineCacheRunning)
+        this.pipelineCached = true;
+        this.pipelineCachedChanged.emit(this.pipelineCached);
+      });
     });
   }
 
diff --git a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
index 5660ba1..4041746 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
@@ -18,13 +18,14 @@
 
 import {Component, Input, OnInit} from "@angular/core";
 import {DialogRef} from "../../../core-ui/dialog/base-dialog/dialog-ref";
-import {Message, Pipeline} from "../../../core-model/gen/streampipes-model";
+import {Message, Pipeline, PipelineCanvasMetadata} from "../../../core-model/gen/streampipes-model";
 import {ObjectProvider} from "../../services/object-provider.service";
 import {EditorService} from "../../services/editor.service";
 import {PipelineService} from "../../../platform-services/apis/pipeline.service";
 import {ShepherdService} from "../../../services/tour/shepherd.service";
 import {FormControl, FormGroup, Validators} from "@angular/forms";
 import {Router} from "@angular/router";
+import {PipelineCanvasMetadataService} from "../../../platform-services/apis/pipeline-canvas-metadata.service";
 
 @Component({
   selector: 'save-pipeline',
@@ -48,20 +49,24 @@ export class SavePipelineComponent implements OnInit {
   @Input()
   currentModifiedPipelineId: string;
 
+  @Input()
+  pipelineCanvasMetadata: PipelineCanvasMetadata;
+
   saving: boolean = false;
   saved: boolean = false;
 
   storageError: boolean = false;
   errorMessage: string = '';
 
- currentPipelineName: string;
+  currentPipelineName: string;
 
   constructor(private editorService: EditorService,
               private dialogRef: DialogRef<SavePipelineComponent>,
               private objectProvider: ObjectProvider,
               private pipelineService: PipelineService,
               private Router: Router,
-              private ShepherdService: ShepherdService) {
+              private ShepherdService: ShepherdService,
+              private pipelineCanvasService: PipelineCanvasMetadataService) {
     this.pipelineCategories = [];
     this.updateMode = "update";
   }
@@ -112,8 +117,9 @@ export class SavePipelineComponent implements OnInit {
 
   savePipeline(switchTab) {
     let storageRequest;
+    let updateMode = this.currentModifiedPipelineId && this.updateMode === 'update';
 
-    if (this.currentModifiedPipelineId && this.updateMode === 'update') {
+    if (updateMode) {
       storageRequest = this.pipelineService.updatePipeline(this.pipeline);
     } else {
       this.pipeline._id = undefined;
@@ -124,6 +130,7 @@ export class SavePipelineComponent implements OnInit {
         .subscribe(statusMessage => {
           if (statusMessage.success) {
             let pipelineId: string = statusMessage.notifications[1].description;
+            this.storePipelineCanvasMetadata(pipelineId, updateMode);
             this.afterStorage(statusMessage, switchTab, pipelineId);
           } else {
             this.displayErrors(statusMessage.notifications[0]);
@@ -133,6 +140,21 @@ export class SavePipelineComponent implements OnInit {
         });
   };
 
+  storePipelineCanvasMetadata(pipelineId: string,
+                              updateMode: boolean) {
+    let request;
+    this.pipelineCanvasMetadata.pipelineId = pipelineId;
+    if (updateMode) {
+      request = this.pipelineCanvasService.updatePipelineCanvasMetadata(this.pipelineCanvasMetadata);
+    } else {
+      this.pipelineCanvasMetadata._id = undefined;
+      this.pipelineCanvasMetadata._rev = undefined;
+      request = this.pipelineCanvasService.addPipelineCanvasMetadata(this.pipelineCanvasMetadata);
+    }
+
+    request.subscribe();
+  }
+
   afterStorage(statusMessage: Message, switchTab, pipelineId?: string) {
     this.hide();
     this.editorService.makePipelineAssemblyEmpty(true);
diff --git a/ui/src/app/editor/services/editor.service.ts b/ui/src/app/editor/services/editor.service.ts
index 18b115d..a3f244a 100644
--- a/ui/src/app/editor/services/editor.service.ts
+++ b/ui/src/app/editor/services/editor.service.ts
@@ -21,9 +21,12 @@ import {HttpClient} from "@angular/common/http";
 import {
   DataProcessorInvocation,
   DataSetModificationMessage,
-  DataSinkInvocation, Pipeline,
+  DataSinkInvocation,
+  Pipeline,
+  PipelineCanvasMetadata,
   PipelineElementRecommendationMessage,
-  PipelineModificationMessage, PipelinePreviewModel,
+  PipelineModificationMessage,
+  PipelinePreviewModel,
   SpDataSet,
   SpDataStream
 } from "../../core-model/gen/streampipes-model";
@@ -79,6 +82,13 @@ export class EditorService {
             }));
     }
 
+    getCachedPipelineCanvasMetadata(): Observable<PipelineCanvasMetadata> {
+      return this.http.get(this.platformServicesCommons.authUserBasePath() + "/pipeline-canvas-cache")
+          .pipe(map(response => {
+            return PipelineCanvasMetadata.fromData(response as any);
+      }));
+    }
+
     convert(payload: any) {
       if (payload['@class'] === "org.apache.streampipes.model.SpDataSet") {
         return SpDataSet.fromData(payload as SpDataSet);
@@ -115,10 +125,19 @@ export class EditorService {
         return this.http.post(this.platformServicesCommons.authUserBasePath() + "/pipeline-cache", rawPipelineModel);
     }
 
+    updateCachedCanvasMetadata(pipelineCanvasMetadata: PipelineCanvasMetadata) {
+      return this.http.post(this.platformServicesCommons.authUserBasePath()
+          + "/pipeline-canvas-cache", pipelineCanvasMetadata)
+    }
+
     removePipelineFromCache() {
         return this.http.delete(this.platformServicesCommons.authUserBasePath() + "/pipeline-cache");
     }
 
+    removeCanvasMetadataFromCache() {
+      return this.http.delete(this.platformServicesCommons.authUserBasePath() + "/pipeline-canvas-cache");
+    }
+
     private get pipelinesResourceUrl() {
         return this.platformServicesCommons.authUserBasePath() + '/pipelines'
     }
diff --git a/ui/src/app/editor/services/pipeline-positioning.service.ts b/ui/src/app/editor/services/pipeline-positioning.service.ts
index ef03614..bf41d28 100644
--- a/ui/src/app/editor/services/pipeline-positioning.service.ts
+++ b/ui/src/app/editor/services/pipeline-positioning.service.ts
@@ -25,6 +25,8 @@ import {PipelineElementConfig} from "../model/editor.model";
 import {
     DataProcessorInvocation,
     DataSinkInvocation,
+    PipelineCanvasMetadata,
+    PipelineElementMetadata,
     SpDataStream
 } from "../../core-model/gen/streampipes-model";
 import {JsplumbFactoryService} from "./jsplumb-factory.service";
@@ -39,10 +41,38 @@ export class PipelinePositioningService {
                 private ObjectProvider: ObjectProvider) {
     }
 
+    collectPipelineElementPositions(pipelineCanvasMetadata: PipelineCanvasMetadata,
+                                    rawPipelineModel: PipelineElementConfig[]): PipelineCanvasMetadata {
+        rawPipelineModel.forEach(pe => {
+           this.collectPipelineElementPosition(pe.payload.dom, pipelineCanvasMetadata);
+        });
+        return pipelineCanvasMetadata;
+    }
+
+    collectPipelineElementPosition(domId: string,
+                                   pipelineCanvasMetadata: PipelineCanvasMetadata) {
+        let elementRef = $(`#${domId}`);
+        if (elementRef && elementRef.position()) {
+            let leftPos = elementRef.position().left;
+            let topPos = elementRef.position().top;
+            if (!pipelineCanvasMetadata.pipelineElementMetadata) {
+                pipelineCanvasMetadata.pipelineElementMetadata = {};
+            }
+            if (!pipelineCanvasMetadata.pipelineElementMetadata[domId]) {
+                pipelineCanvasMetadata.pipelineElementMetadata[domId] = new PipelineElementMetadata();
+            }
+            pipelineCanvasMetadata.pipelineElementMetadata[domId].position = {
+                x: leftPos,
+                y: topPos
+            };
+        }
+    }
+
     displayPipeline(rawPipelineModel: PipelineElementConfig[],
                     targetCanvas,
                     previewConfig: boolean,
-                    autoLayout: boolean) {
+                    autoLayout: boolean,
+                    pipelineCanvasMetadata?: PipelineCanvasMetadata) {
         let jsPlumbBridge = this.JsplumbFactoryService.getJsplumbBridge(previewConfig);
         let jsplumbConfig = previewConfig ? this.JsplumbConfigService.getPreviewConfig() : this.JsplumbConfigService.getEditorConfig();
         rawPipelineModel.forEach(currentPe => {
@@ -65,44 +95,60 @@ export class PipelinePositioningService {
         this.connectPipelineElements(rawPipelineModel, previewConfig, jsplumbConfig, jsPlumbBridge);
         if (autoLayout) {
             this.layoutGraph(targetCanvas, "span[id^='jsplumb']", previewConfig ? 75 : 110, previewConfig);
+        } else if (pipelineCanvasMetadata)  {
+            this.layoutGraphFromCanvasMetadata(pipelineCanvasMetadata);
         }
         jsPlumbBridge.repaintEverything();
     }
 
-    layoutGraph(canvas, nodeIdentifier, dimension, isPreview) {
-        let jsPlumbBridge = this.JsplumbFactoryService.getJsplumbBridge(isPreview);
-        var g = new dagre.graphlib.Graph();
-        g.setGraph({rankdir: "LR", ranksep: isPreview ? "50" : "100"});
+    layoutGraph(canvasId: string,
+                nodeIdentifier: string,
+                dimension: number,
+                previewConfig: boolean) {
+        let jsPlumbBridge = this.JsplumbFactoryService.getJsplumbBridge(previewConfig);
+        let g = new dagre.graphlib.Graph();
+        g.setGraph({rankdir: "LR", ranksep: previewConfig ? "50" : "100"});
         g.setDefaultEdgeLabel(function () {
             return {};
         });
 
-        var nodes = $(canvas).find(nodeIdentifier).get();
-        nodes.forEach((n, index) => {
+        let nodes = $(canvasId).find(nodeIdentifier).get();
+        nodes.forEach((n) => {
             g.setNode(n.id, {label: n.id, width: dimension, height: dimension});
         });
 
-        var edges = jsPlumbBridge.getAllConnections();
+        let edges = jsPlumbBridge.getAllConnections();
         edges.forEach(edge => {
             g.setEdge(edge.source.id, edge.target.id);
         });
 
         dagre.layout(g);
         g.nodes().forEach(v => {
-            $(`#${v}`).css("left", g.node(v).x + "px");
-            $(`#${v}`).css("top", g.node(v).y + "px");
+            let elementRef = $(`#${v}`);
+            elementRef.css("left", g.node(v).x + "px");
+            elementRef.css("top", g.node(v).y + "px");
         });
     }
 
+    layoutGraphFromCanvasMetadata(pipelineCanvasMetadata: PipelineCanvasMetadata) {
+        Object.entries(pipelineCanvasMetadata.pipelineElementMetadata).forEach(
+            ([key, value]) => {
+                let elementRef = $(`#${key}`);
+                if (elementRef) {
+                    elementRef.css("left", value.position.x + "px");
+                    elementRef.css("top", value.position.y + "px");
+                }
+            }
+        );
+    }
+
     connectPipelineElements(rawPipelineModel: PipelineElementConfig[],
                             previewConfig: boolean,
                             jsplumbConfig: any,
                             jsPlumbBridge: JsplumbBridge) {
-        var source, target;
+        let source, target;
         jsPlumbBridge.setSuspendDrawing(true);
-        for (var i = 0; i < rawPipelineModel.length; i++) {
-            var pe = rawPipelineModel[i];
-
+        rawPipelineModel.forEach(pe => {
             if (pe.type == "sepa" || pe.type == "action") {
                 if (!(pe.settings.disabled) && pe.payload.connectedTo) {
                     pe.payload.connectedTo.forEach((connection, index) => {
@@ -127,7 +173,7 @@ export class PipelinePositioningService {
                     });
                 }
             }
-        }
+        });
         jsPlumbBridge.setSuspendDrawing(false, true);
     }
 }
diff --git a/ui/src/app/platform-services/apis/pipeline-canvas-metadata.service.ts b/ui/src/app/platform-services/apis/pipeline-canvas-metadata.service.ts
new file mode 100644
index 0000000..09d1d5c
--- /dev/null
+++ b/ui/src/app/platform-services/apis/pipeline-canvas-metadata.service.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import {Injectable} from "@angular/core";
+import {HttpClient} from "@angular/common/http";
+import {Observable} from "rxjs";
+import {
+  DataProcessorInvocation,
+  DataSinkInvocation,
+  DataSourceDescription, PipelineCanvasMetadata, SpDataSet, SpDataStream
+} from "../../core-model/gen/streampipes-model";
+import {PlatformServicesCommons} from "./commons.service";
+import {map} from "rxjs/operators";
+
+@Injectable()
+export class PipelineCanvasMetadataService {
+
+  constructor(private http: HttpClient,
+              private platformServicesCommons: PlatformServicesCommons) {
+
+  }
+
+  addPipelineCanvasMetadata(pipelineCanvasMetadata: PipelineCanvasMetadata) {
+    return this.http.post(this.pipelineCanvasMetadataBasePath, pipelineCanvasMetadata);
+  }
+
+  getPipelineCanvasMetadata(pipelineId: string): Observable<PipelineCanvasMetadata> {
+    return this.http.get(this.pipelineCanvasMetadataPipelinePath
+        + pipelineId).pipe(map(response => PipelineCanvasMetadata.fromData(response as any)));
+  }
+
+  updatePipelineCanvasMetadata(pipelineCanvasMetadata: PipelineCanvasMetadata) {
+    return this.http.put(this.pipelineCanvasMetadataBasePath
+        + "/"
+        + pipelineCanvasMetadata.pipelineId, pipelineCanvasMetadata);
+  }
+
+  deletePipelineCanvasMetadata(pipelineId: string) {
+    return this.http.delete(this.pipelineCanvasMetadataPipelinePath
+        + pipelineId);
+  }
+
+  private get pipelineCanvasMetadataBasePath() {
+    return this.platformServicesCommons.authUserBasePath() + "/pipeline-canvas-metadata";
+  }
+
+  private get pipelineCanvasMetadataPipelinePath() {
+    return this.pipelineCanvasMetadataBasePath + "/pipeline/";
+  }
+}
diff --git a/ui/src/app/platform-services/platform.module.ts b/ui/src/app/platform-services/platform.module.ts
index 44c8bea..85800cd 100644
--- a/ui/src/app/platform-services/platform.module.ts
+++ b/ui/src/app/platform-services/platform.module.ts
@@ -26,6 +26,7 @@ import {MeasurementUnitsService} from "./apis/measurement-units.service";
 import {PipelineElementTemplateService} from "./apis/pipeline-element-template.service";
 import {PipelineMonitoringService} from "./apis/pipeline-monitoring.service";
 import {SemanticTypesService} from "./apis/semantic-types.service";
+import {PipelineCanvasMetadataService} from "./apis/pipeline-canvas-metadata.service";
 
 @NgModule({
   imports: [],
@@ -34,6 +35,7 @@ import {SemanticTypesService} from "./apis/semantic-types.service";
     FilesService,
     MeasurementUnitsService,
     PlatformServicesCommons,
+    PipelineCanvasMetadataService,
     PipelineElementEndpointService,
     PipelineElementTemplateService,
     //PipelineTemplateService,