From f1e4eb0fa3f801de80678ad83060a561f48915c3 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Fri, 14 Mar 2025 13:21:42 +0100
Subject: [PATCH] export of glyphs added

---
 .../CellDesignerTestFunctions.java            |  24 +++
 .../model/celldesigner/ProjectExportTest.java |  17 ++
 .../testFiles/glyphs/uni.png                  | Bin 0 -> 25059 bytes
 .../converter/ComplexZipConverter.java        |  67 +++++--
 .../mapviewer/converter/OverviewParser.java   | 179 +++++++-----------
 .../mapviewer/converter/ProjectFactory.java   |  52 +++--
 .../converter/ComplexZipConverterTest.java    |  27 +--
 .../converter/OverviewParserTest.java         |  77 ++++----
 .../converter/ProjectFactoryTest.java         |   4 +-
 .../commands/ColorModelCommandTest.java       |  56 +++---
 .../mapviewer/model/ProjectComparator.java    |  15 +-
 .../cache/UploadedFileEntryComparator.java    |  32 ++++
 .../map/layout/graphics/GlyphComparator.java  |  24 +++
 .../model/map/species/ElementComparator.java  |   7 +
 14 files changed, 356 insertions(+), 225 deletions(-)
 create mode 100644 converter-CellDesigner/testFiles/glyphs/uni.png
 create mode 100644 model/src/main/java/lcsb/mapviewer/model/cache/UploadedFileEntryComparator.java
 create mode 100644 model/src/main/java/lcsb/mapviewer/model/map/layout/graphics/GlyphComparator.java

diff --git a/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/CellDesignerTestFunctions.java b/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/CellDesignerTestFunctions.java
index aa357bbda2..24e3bebbca 100644
--- a/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/CellDesignerTestFunctions.java
+++ b/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/CellDesignerTestFunctions.java
@@ -7,6 +7,7 @@ import lcsb.mapviewer.converter.ConverterParams;
 import lcsb.mapviewer.converter.InvalidInputDataExecption;
 import lcsb.mapviewer.converter.model.celldesigner.structure.CellDesignerSpecies;
 import lcsb.mapviewer.model.Project;
+import lcsb.mapviewer.model.cache.UploadedFileEntry;
 import lcsb.mapviewer.model.graphics.HorizontalAlign;
 import lcsb.mapviewer.model.graphics.VerticalAlign;
 import lcsb.mapviewer.model.map.Drawable;
@@ -20,6 +21,7 @@ import lcsb.mapviewer.model.map.compartment.TopSquareCompartment;
 import lcsb.mapviewer.model.map.kinetics.SbmlFunction;
 import lcsb.mapviewer.model.map.kinetics.SbmlParameter;
 import lcsb.mapviewer.model.map.kinetics.SbmlUnit;
+import lcsb.mapviewer.model.map.layout.graphics.Glyph;
 import lcsb.mapviewer.model.map.layout.graphics.Layer;
 import lcsb.mapviewer.model.map.layout.graphics.LayerText;
 import lcsb.mapviewer.model.map.model.Model;
@@ -46,9 +48,13 @@ import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.geom.Point2D;
 import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 
 import static org.junit.Assert.assertEquals;
 
@@ -299,4 +305,22 @@ public abstract class CellDesignerTestFunctions extends TestUtils {
     return project;
   }
 
+  protected Glyph createGlyph() throws IOException {
+    Glyph glyph = new Glyph();
+    glyph.setFile(createFile("testFiles/glyphs/uni.png"));
+
+    return glyph;
+  }
+
+  private UploadedFileEntry createFile(final String filePath) throws IOException {
+    UploadedFileEntry uploadedFileEntry = new UploadedFileEntry();
+    byte[] byteArray = Files.readAllBytes(Paths.get(filePath));
+    uploadedFileEntry.setFileContent(byteArray);
+    uploadedFileEntry.setOriginalFileName("glyphs/" + new File(filePath).getName());
+    uploadedFileEntry.setLength(byteArray.length);
+    uploadedFileEntry.setLocalPath(null);
+    return uploadedFileEntry;
+
+  }
+
 }
diff --git a/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/ProjectExportTest.java b/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/ProjectExportTest.java
index 57001079c8..1f5034282d 100644
--- a/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/ProjectExportTest.java
+++ b/converter-CellDesigner/src/test/java/lcsb/mapviewer/converter/model/celldesigner/ProjectExportTest.java
@@ -7,6 +7,7 @@ import lcsb.mapviewer.converter.zip.ZipEntryFile;
 import lcsb.mapviewer.converter.zip.ZipEntryFileFactory;
 import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.ProjectComparator;
+import lcsb.mapviewer.model.map.layout.graphics.Glyph;
 import lcsb.mapviewer.model.map.model.ElementSubmodelConnection;
 import lcsb.mapviewer.model.map.model.Model;
 import lcsb.mapviewer.model.map.model.ModelData;
@@ -99,6 +100,22 @@ public class ProjectExportTest extends CellDesignerTestFunctions {
     testSerializationOverZip(project);
   }
 
+  @Test
+  public void testGlyph() throws Exception {
+    Glyph glyph = createGlyph();
+    Project project = createProject();
+    Model model = createEmptyModel();
+    model.setWidth(260);
+    Protein protein = createProtein();
+    protein.setGlyph(glyph);
+    model.addElement(protein);
+    project.addModel(model);
+    project.addGlyph(glyph);
+
+    testSerializationOverZip(project);
+  }
+
+
   private void testSerializationOverZip(final Project project) throws Exception {
     File tempFile = File.createTempFile("CD-", ".zip");
     try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
diff --git a/converter-CellDesigner/testFiles/glyphs/uni.png b/converter-CellDesigner/testFiles/glyphs/uni.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd325269723c196ab52460e32a904db0e3bdc46f
GIT binary patch
literal 25059
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

literal 0
HcmV?d00001

diff --git a/converter/src/main/java/lcsb/mapviewer/converter/ComplexZipConverter.java b/converter/src/main/java/lcsb/mapviewer/converter/ComplexZipConverter.java
index 5d6bc2741c..c00117499d 100644
--- a/converter/src/main/java/lcsb/mapviewer/converter/ComplexZipConverter.java
+++ b/converter/src/main/java/lcsb/mapviewer/converter/ComplexZipConverter.java
@@ -10,9 +10,11 @@ import lcsb.mapviewer.converter.zip.LayoutZipEntryFile;
 import lcsb.mapviewer.converter.zip.ModelZipEntryFile;
 import lcsb.mapviewer.converter.zip.ZipEntryFile;
 import lcsb.mapviewer.converter.zip.ZipEntryFileFactory;
+import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.cache.UploadedFileEntry;
 import lcsb.mapviewer.model.map.compartment.Compartment;
 import lcsb.mapviewer.model.map.layout.ReferenceGenomeType;
+import lcsb.mapviewer.model.map.layout.graphics.Glyph;
 import lcsb.mapviewer.model.map.model.ElementSubmodelConnection;
 import lcsb.mapviewer.model.map.model.Model;
 import lcsb.mapviewer.model.map.model.ModelSubmodelConnection;
@@ -40,6 +42,7 @@ import java.lang.reflect.Modifier;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
@@ -65,6 +68,7 @@ public class ComplexZipConverter {
    */
   private static final Logger logger = LogManager.getLogger();
   protected static final String IMMEDIATE_LINK_PREFIX = "IMMEDIATE_LINK:";
+  protected static final String GLYPH_PREFIX = "Glyph:";
 
   /**
    * Class used to create single submap from a file.
@@ -97,7 +101,7 @@ public class ComplexZipConverter {
    * @return complex {@link Model} created from input data
    * @throws InvalidInputDataExecption thrown when there is a problem with accessing input data
    */
-  public Model createModel(final ComplexZipConverterParams params) throws InvalidInputDataExecption, ConverterException {
+  public Model createModel(final ComplexZipConverterParams params, final Project project) throws InvalidInputDataExecption, ConverterException {
     try {
       ZipFile zipFile = params.getZipFile();
       Enumeration<? extends ZipEntry> entries;
@@ -154,16 +158,22 @@ public class ComplexZipConverter {
           processReaction(mapping, nameModelMap, result, reaction);
         }
         for (final Species species : mappingModel.getSpeciesList()) {
-          processSpecies(species, nameModelMap);
+          processSpecies(species, nameModelMap, project.getGlyphs());
         }
       }
+
+      project.addModel(result);
+      for (final Model model : result.getSubmodels()) {
+        project.addModel(model);
+      }
+
       return result;
     } catch (final IOException e) {
       throw new InvalidArgumentException(e);
     }
   }
 
-  private void processSpecies(final Species species, final Map<String, Model> nameModelMap) {
+  private void processSpecies(final Species species, final Map<String, Model> nameModelMap, final List<Glyph> glyphs) {
     String notes = species.getNotes();
     if (notes != null) {
       String[] lines = notes.split("\n");
@@ -172,28 +182,49 @@ public class ComplexZipConverter {
         if (line.startsWith(IMMEDIATE_LINK_PREFIX)) {
           String link = line.replace(IMMEDIATE_LINK_PREFIX, "").trim();
 
-          Compartment compartment = species.getCompartment();
-          if (compartment == null) {
-            logger.warn("[SUBMODEL MAPPING] Species {} in mapping file doesn't start inside compartment. Skipped. Link skipped",
-                species.getElementId());
-          } else {
-            String modelName = compartment.getName().toLowerCase();
-            Model model = nameModelMap.get(modelName);
-            if (model == null) {
-              throw new InvalidArgumentException("Mapping file references to " + modelName + " submodel. But such model doesn't exist");
-            }
-            Element elementToChange = model.getElementByElementId(species.getName());
-            if (elementToChange == null) {
-              throw new InvalidArgumentException("Mapping file references to element with alias: " + species.getName()
-                  + ". But such element doesn't exist");
-            }
+          Element elementToChange = getElementToChange(species, nameModelMap);
+          if (elementToChange != null) {
             elementToChange.setImmediateLink(link);
           }
+        } else if (line.startsWith(GLYPH_PREFIX)) {
+          String filename = line.replace(GLYPH_PREFIX, "").trim();
+
+          Element elementToChange = getElementToChange(species, nameModelMap);
+          if (elementToChange != null) {
+            for (Glyph glyph : glyphs) {
+              if (glyph.getFile().getOriginalFileName().equalsIgnoreCase(filename)) {
+                elementToChange.setGlyph(glyph);
+              }
+            }
+          }
         }
       }
     }
   }
 
+  private static Element getElementToChange(
+      final Species species,
+      final Map<String, Model> nameModelMap) {
+    Compartment compartment = species.getCompartment();
+    if (compartment == null) {
+      logger.warn("[SUBMODEL MAPPING] Species {} in mapping file doesn't start inside compartment. Skipped. Link skipped",
+          species.getElementId());
+    } else {
+      String modelName = compartment.getName().toLowerCase();
+      Model model = nameModelMap.get(modelName);
+      if (model == null) {
+        throw new InvalidArgumentException("Mapping file references to " + modelName + " submodel. But such model doesn't exist");
+      }
+      Element result = model.getElementByElementId(species.getName());
+      if (result == null) {
+        throw new InvalidArgumentException("Mapping file references to element with alias: " + species.getName()
+            + ". But such element doesn't exist");
+      }
+      return result;
+    }
+    return null;
+  }
+
   protected boolean isIgnoredFile(final String name) {
     if (name == null) {
       return true;
diff --git a/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java b/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java
index 6be2c06671..00f51f01df 100644
--- a/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java
+++ b/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java
@@ -1,5 +1,23 @@
 package lcsb.mapviewer.converter;
 
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import lcsb.mapviewer.common.exception.InvalidArgumentException;
+import lcsb.mapviewer.converter.zip.ImageZipEntryFile;
+import lcsb.mapviewer.converter.zip.OverviewLinkDeserializer;
+import lcsb.mapviewer.model.map.OverviewImage;
+import lcsb.mapviewer.model.map.OverviewImageLink;
+import lcsb.mapviewer.model.map.OverviewLink;
+import lcsb.mapviewer.model.map.OverviewModelLink;
+import lcsb.mapviewer.model.map.OverviewSearchLink;
+import lcsb.mapviewer.model.map.model.ModelData;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.imageio.ImageIO;
 import java.awt.Polygon;
 import java.awt.geom.Area;
 import java.awt.geom.Point2D;
@@ -20,33 +38,11 @@ import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
-import javax.imageio.ImageIO;
-
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-
-import lcsb.mapviewer.common.exception.InvalidArgumentException;
-import lcsb.mapviewer.converter.zip.ImageZipEntryFile;
-import lcsb.mapviewer.converter.zip.OverviewLinkDeserializer;
-import lcsb.mapviewer.model.map.OverviewImage;
-import lcsb.mapviewer.model.map.OverviewImageLink;
-import lcsb.mapviewer.model.map.OverviewLink;
-import lcsb.mapviewer.model.map.OverviewModelLink;
-import lcsb.mapviewer.model.map.OverviewSearchLink;
-import lcsb.mapviewer.model.map.model.Model;
-
 /**
  * Parser used to extract data about {@link OverviewImage overview images} from
  * zip file.
- * 
+ *
  * @author Piotr Gawron
- * 
  */
 public class OverviewParser {
   /**
@@ -58,37 +54,17 @@ public class OverviewParser {
 
   private static final String JSON_COORDINATES_FILENAME = "coords.json";
 
-  /**
-   * Name of the column in {@link #COORDINATES_FILENAME} where information about
-   * {@link OverviewModelLink#zoomLevel} is stored.
-   */
   private static final String ZOOM_LEVEL_COORDINATES_COLUMN = "MODEL_ZOOM_LEVEL";
-  /**
-   * Name of the column in {@link #COORDINATES_FILENAME} where information about
-   * {@link OverviewModelLink#xCoord},{@link OverviewModelLink#yCoord} is stored.
-   */
+
   private static final String REDIRECTION_COORDINATES_COORDINATE_COLUMN = "MODEL_COORDINATES";
-  /**
-   * Name of the column in {@link #COORDINATES_FILENAME} where information about
-   * {@link OverviewModelLink#linkedModel} or
-   * {@link OverviewImageLink#linkedOverviewImage} is stored.
-   */
+
   private static final String TARGET_FILENAME_COORDINATE_COLUMN = "LINK_TARGET";
-  /**
-   * Name of the column in {@link #COORDINATES_FILENAME} where information about
-   * {@link OverviewLink#polygon} is stored.
-   */
+
   private static final String POLYGON_COORDINATE_COLUMN = "POLYGON";
-  /**
-   * Name of the column in {@link #COORDINATES_FILENAME} where information about
-   * {@link OverviewLink#overviewImage source of the image} is stored.
-   */
+
   private static final String FILENAME_COORDINATE_COLUMN = "FILE";
 
-  /**
-   * Name of the column in {@link #COORDINATES_FILENAME} where information about
-   * type of the link (implementation of {@link OverviewLink} class) is stored.
-   */
+
   private static final String TARGET_TYPE_COORDINATE_COLUMN = "LINK_TYPE";
 
   /**
@@ -126,20 +102,16 @@ public class OverviewParser {
    * Method that parse zip file and creates list of {@link OverviewImage images}
    * from it.
    *
-   * @param models
-   *          map with models where the key is name of the file and value is model
-   *          that was parsed from the file
-   * @param files
-   *          list with files to parse
-   * @param outputDirectory
-   *          directory where images should be stored, directory path should be
-   *          absolute
+   * @param models          map with models where the key is name of the file and value is model
+   *                        that was parsed from the file
+   * @param files           list with files to parse
+   * @param outputDirectory directory where images should be stored, directory path should be
+   *                        absolute
    * @return list of {@link OverviewImage images}
-   * @throws InvalidOverviewFile
-   *           thrown when the zip file contains invalid data
+   * @throws InvalidOverviewFile thrown when the zip file contains invalid data
    */
-  public List<OverviewImage> parseOverviewLinks(final Set<Model> models, final List<ImageZipEntryFile> files,
-      final String outputDirectory, final ZipFile zipFile) throws InvalidOverviewFile {
+  public List<OverviewImage> parseOverviewLinks(final Set<ModelData> models, final List<ImageZipEntryFile> files,
+                                                final String outputDirectory, final ZipFile zipFile) throws InvalidOverviewFile {
     if (outputDirectory != null) {
       File f = new File(outputDirectory);
       if (!f.exists()) {
@@ -220,9 +192,9 @@ public class OverviewParser {
     }
   }
 
-  private Map<String, Model> createMapping(final Set<Model> models) {
-    Map<String, Model> result = new HashMap<>();
-    for (final Model model : models) {
+  private Map<String, ModelData> createMapping(final Set<ModelData> models) {
+    Map<String, ModelData> result = new HashMap<>();
+    for (final ModelData model : models) {
       result.put(model.getName().toLowerCase(), model);
     }
     return result;
@@ -232,20 +204,16 @@ public class OverviewParser {
    * This method process data from {@link #COORDINATES_FILENAME} in zip archive.
    * This method adds connections between images and between images and models.
    *
-   * @param models
-   *          map with models where the key is name of the file and value is model
-   *          that was parsed from the file
-   * @param images
-   *          list of {@link OverviewImage images} that should be connected
-   * @param coordinatesData
-   *          {@link String} with the data taken from
-   *          {@link #COORDINATES_FILENAME} file
-   * @throws InvalidOverviewFile
-   *           thrown when the data are invalid
+   * @param models          map with models where the key is name of the file and value is model
+   *                        that was parsed from the file
+   * @param images          list of {@link OverviewImage images} that should be connected
+   * @param coordinatesData {@link String} with the data taken from
+   *                        {@link #COORDINATES_FILENAME} file
+   * @throws InvalidOverviewFile thrown when the data are invalid
    */
-  protected List<OverviewImage> processCoordinates(final Set<Model> models, final List<OverviewImage> images, final String coordinatesData)
+  protected List<OverviewImage> processCoordinates(final Set<ModelData> models, final List<OverviewImage> images, final String coordinatesData)
       throws InvalidOverviewFile {
-    Map<String, Model> modelMapping = createMapping(models);
+    Map<String, ModelData> modelMapping = createMapping(models);
     String[] rows = coordinatesData.replaceAll("\r", "\n").split("\n");
     Integer filenameColumn = null;
     Integer polygonColumn = null;
@@ -399,9 +367,9 @@ public class OverviewParser {
     return images;
   }
 
-  protected List<OverviewImage> processJsonCoordinates(final Set<Model> models, final List<OverviewImage> images, final String json)
+  protected List<OverviewImage> processJsonCoordinates(final Set<ModelData> models, final List<OverviewImage> images, final String json)
       throws InvalidOverviewFile {
-    Map<String, Model> modelMapping = createMapping(models);
+    Map<String, ModelData> modelMapping = createMapping(models);
     ObjectMapper mapper = new ObjectMapper();
     final SimpleModule module = new SimpleModule();
     module.addDeserializer(OverviewLink.class, new OverviewLinkDeserializer());
@@ -429,7 +397,7 @@ public class OverviewParser {
                 ((OverviewImageLink) link).setLinkedOverviewImage(linkImage);
               }
               if (link instanceof OverviewModelLink) {
-                Model model = modelMapping.get(((OverviewModelLink) link).getLinkedModel().getName().toLowerCase());
+                ModelData model = modelMapping.get(((OverviewModelLink) link).getLinkedModel().getName().toLowerCase());
 
                 if (model == null) {
                   throw new InvalidOverviewFile(((OverviewModelLink) link).getLinkedModel().getName() + " is missing");
@@ -454,35 +422,32 @@ public class OverviewParser {
    * Creates a link from parameters and place it in appropriate
    * {@link OverviewImage}.
    *
-   * @param filename
-   *          {@link OverviewImage#filename name of the image}
-   * @param polygon
-   *          {@link OverviewImage#polygon polygon} describing link
-   * @param linkTarget
-   *          defines target that should be invoked when the link is activated.
-   *          This target is either a file name (in case of
-   *          {@link #MODEL_LINK_TYPE} or {@link #IMAGE_LINK_TYPE}) or a search
-   *          string (in case of {@link #SEARCH_LINK_TYPE}).
-   * @param coord
-   *          coordinates on the model where redirection should be placed in case
-   *          of {@link #MODEL_LINK_TYPE} connection
-   * @param zoomLevel
-   *          zoom level on the model where redirection should be placed in case
-   *          of {@link #MODEL_LINK_TYPE} connection
-   * @param linkType
-   *          type of the connection. This will define implementation of
-   *          {@link OverviewImage} that will be used. For now three values are
-   *          acceptable: {@link #MODEL_LINK_TYPE}, {@link #IMAGE_LINK_TYPE},
-   *          {@link #SEARCH_LINK_TYPE}.
-   * @param images
-   *          list of images that are available
-   * @param models
-   *          list of models that are available
-   * @throws InvalidCoordinatesFile
-   *           thrown when one of the input parameters is invalid
+   * @param filename   filename name of the image
+   * @param polygon    polygon polygon describing link
+   * @param linkTarget defines target that should be invoked when the link is activated.
+   *                   This target is either a file name (in case of
+   *                   {@link #MODEL_LINK_TYPE} or {@link #IMAGE_LINK_TYPE}) or a search
+   *                   string (in case of {@link #SEARCH_LINK_TYPE}).
+   * @param coord      coordinates on the model where redirection should be placed in case
+   *                   of {@link #MODEL_LINK_TYPE} connection
+   * @param zoomLevel  zoom level on the model where redirection should be placed in case
+   *                   of {@link #MODEL_LINK_TYPE} connection
+   * @param linkType   type of the connection. This will define implementation of
+   *                   {@link OverviewImage} that will be used. For now three values are
+   *                   acceptable: {@link #MODEL_LINK_TYPE}, {@link #IMAGE_LINK_TYPE},
+   *                   {@link #SEARCH_LINK_TYPE}.
+   * @param images     list of images that are available
+   * @param models     list of models that are available
+   * @throws InvalidCoordinatesFile thrown when one of the input parameters is invalid
    */
-  private void createOverviewLink(final String filename, final String polygon, final String linkTarget, final String coord, final String zoomLevel,
-      final String linkType, final List<OverviewImage> images, final Map<String, Model> models) throws InvalidCoordinatesFile {
+  private void createOverviewLink(final String filename,
+                                  final String polygon,
+                                  final String linkTarget,
+                                  final String coord,
+                                  final String zoomLevel,
+                                  final String linkType,
+                                  final List<OverviewImage> images,
+                                  final Map<String, ModelData> models) throws InvalidCoordinatesFile {
     OverviewImage image = null;
     for (final OverviewImage oi : images) {
       if (oi.getFilename().equalsIgnoreCase(filename)) {
@@ -494,7 +459,7 @@ public class OverviewParser {
     }
     OverviewLink ol = null;
     if (linkType.equals(MODEL_LINK_TYPE)) {
-      Model model = models.get(linkTarget.toLowerCase());
+      ModelData model = models.get(linkTarget.toLowerCase());
       if (model == null) {
         throw new InvalidCoordinatesFile("Unknown model in \"" + COORDINATES_FILENAME + "\" file: " + linkTarget);
       }
diff --git a/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java b/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java
index b5f450031f..51e60b8766 100644
--- a/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java
+++ b/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java
@@ -7,6 +7,7 @@ import lcsb.mapviewer.converter.zip.ImageZipEntryFile;
 import lcsb.mapviewer.converter.zip.LayoutZipEntryFile;
 import lcsb.mapviewer.converter.zip.ZipEntryFile;
 import lcsb.mapviewer.model.Project;
+import lcsb.mapviewer.model.cache.UploadedFileEntry;
 import lcsb.mapviewer.model.map.InconsistentModelException;
 import lcsb.mapviewer.model.map.compartment.Compartment;
 import lcsb.mapviewer.model.map.compartment.SquareCompartment;
@@ -30,6 +31,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Enumeration;
@@ -72,17 +74,6 @@ public class ProjectFactory {
   public Project create(final ComplexZipConverterParams params, final Project project)
       throws InvalidInputDataExecption, ConverterException {
     try {
-      Model model = converter.createModel(params);
-
-      Set<Model> models = new HashSet<>();
-      models.add(model);
-      models.addAll(model.getSubmodels());
-
-      project.addModel(model);
-      for (final Model m : model.getSubmodels()) {
-        project.addModel(m);
-      }
-
       ZipFile zipFile = params.getZipFile();
       Enumeration<? extends ZipEntry> entries;
 
@@ -103,10 +94,12 @@ public class ProjectFactory {
           }
         }
       }
+      converter.createModel(params, project);
+
+
       if (!imageEntries.isEmpty()) {
         OverviewParser parser = new OverviewParser();
-        project
-            .addOverviewImages(parser.parseOverviewLinks(models, imageEntries, params.getVisualizationDir(), zipFile));
+        project.addOverviewImages(parser.parseOverviewLinks(project.getModels(), imageEntries, params.getVisualizationDir(), zipFile));
       }
       if (!project.getGlyphs().isEmpty()) {
         assignGlyphsToElements(project);
@@ -151,7 +144,7 @@ public class ProjectFactory {
     String[] lines = notes.split("[\n\r]+");
     StringBuilder result = new StringBuilder();
     for (final String line : lines) {
-      if (!line.startsWith("Glyph:")) {
+      if (!line.startsWith(ComplexZipConverter.GLYPH_PREFIX)) {
         result.append(line).append("\n");
       }
     }
@@ -161,8 +154,8 @@ public class ProjectFactory {
   Glyph extractGlyph(final Project project, final String notes) throws InvalidGlyphFile {
     String[] lines = notes.split("[\n\r]+");
     for (final String line : lines) {
-      if (line.startsWith("Glyph:")) {
-        String glyphString = line.replace("Glyph:", "").trim().toLowerCase();
+      if (line.startsWith(ComplexZipConverter.GLYPH_PREFIX)) {
+        String glyphString = line.replace(ComplexZipConverter.GLYPH_PREFIX, "").trim().toLowerCase();
         for (final Glyph glyph : project.getGlyphs()) {
           if (glyph.getFile().getOriginalFileName().toLowerCase().equalsIgnoreCase(glyphString)) {
             return glyph;
@@ -189,12 +182,28 @@ public class ProjectFactory {
 
       addModelFileToZip(mapping, "submaps/mapping." + converter.getFileExtensions().get(0), converter, zos);
 
+      for (ModelData model : project.getModels()) {
+        addGlyphsToZip(model.getModel(), zos);
+      }
+
     } catch (IOException ioe) {
       throw new ConverterException(ioe);
     }
     return byteArrayOutputStream.toByteArray();
   }
 
+  private void addGlyphsToZip(final Model model, final ZipOutputStream zos) throws IOException {
+    for (Element element : model.getElements()) {
+      if (element.getGlyph() != null) {
+        UploadedFileEntry uploadedFileEntry = element.getGlyph().getFile();
+        ZipEntry entry = new ZipEntry("glyphs/" + new File(uploadedFileEntry.getOriginalFileName()).getName());
+        zos.putNextEntry(entry);
+        zos.write(uploadedFileEntry.getFileContent());
+        zos.closeEntry();
+      }
+    }
+  }
+
   private int idCounter = 0;
 
   private Model createMappingModel(final Set<ModelData> models) {
@@ -228,7 +237,16 @@ public class ProjectFactory {
 
           Species sourceElement = getCompartmentChildForConnection(parentElement.getElementId(), mappingParentCompartment);
           String notes = sourceElement.getNotes();
-          notes += ComplexZipConverter.IMMEDIATE_LINK_PREFIX + parentElement.getImmediateLink() + "\n";
+          notes += "\n" + ComplexZipConverter.IMMEDIATE_LINK_PREFIX + parentElement.getImmediateLink();
+          sourceElement.setNotes(notes);
+        }
+        if (parentElement.getGlyph() != null) {
+          UploadedFileEntry uploadedFileEntry = parentElement.getGlyph().getFile();
+          Compartment mappingParentCompartment = getCompartmentForConnection(parentModel.getName(), mapping);
+
+          Species sourceElement = getCompartmentChildForConnection(parentElement.getElementId(), mappingParentCompartment);
+          String notes = sourceElement.getNotes();
+          notes += "\n" + ComplexZipConverter.GLYPH_PREFIX + "glyphs/" + new File(uploadedFileEntry.getOriginalFileName()).getName();
           sourceElement.setNotes(notes);
         }
       }
diff --git a/converter/src/test/java/lcsb/mapviewer/converter/ComplexZipConverterTest.java b/converter/src/test/java/lcsb/mapviewer/converter/ComplexZipConverterTest.java
index 5850f0dad1..bea7c1a888 100644
--- a/converter/src/test/java/lcsb/mapviewer/converter/ComplexZipConverterTest.java
+++ b/converter/src/test/java/lcsb/mapviewer/converter/ComplexZipConverterTest.java
@@ -4,6 +4,7 @@ import lcsb.mapviewer.common.exception.InvalidArgumentException;
 import lcsb.mapviewer.common.exception.InvalidClassException;
 import lcsb.mapviewer.converter.zip.LayoutZipEntryFile;
 import lcsb.mapviewer.converter.zip.ModelZipEntryFile;
+import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.map.model.Model;
 import lcsb.mapviewer.model.map.model.ModelSubmodelConnection;
 import lcsb.mapviewer.model.map.model.SubmodelType;
@@ -40,7 +41,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
   }
 
   @Test
-  public void testConstructor3() throws Exception {
+  public void testConstructor3() {
     new ComplexZipConverter(MockConverter.class);
   }
 
@@ -54,7 +55,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s2.xml", "s2", false, false, SubmodelType.PATHWAY));
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", null, false, true, null));
-    Model model = converter.createModel(params);
+    Model model = converter.createModel(params, new Project());
     assertNotNull(model);
     assertEquals("main", model.getName());
     assertEquals(3, model.getSubmodelConnections().size());
@@ -108,7 +109,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s2.xml", "s2", false, false, SubmodelType.PATHWAY));
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", null, false, true, null));
-    Model model = converter.createModel(params);
+    Model model = converter.createModel(params, new Project());
     assertNotNull(model);
     assertEquals("main", model.getName());
     assertEquals(3, model.getSubmodelConnections().size());
@@ -160,7 +161,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s1.xml", "s1", false, false, SubmodelType.DOWNSTREAM_TARGETS));
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", null, false, true, null));
-    converter.createModel(params);
+    converter.createModel(params, new Project());
   }
 
   @Test(expected = InvalidArgumentException.class)
@@ -174,7 +175,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s2.xml", "s2", false, false, SubmodelType.PATHWAY));
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", null, false, true, null));
-    converter.createModel(params);
+    converter.createModel(params, new Project());
   }
 
   @Test(expected = InvalidArgumentException.class)
@@ -188,7 +189,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s2.xml", "s2", false, false, SubmodelType.PATHWAY));
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", null, false, true, null));
-    converter.createModel(params);
+    converter.createModel(params, new Project());
   }
 
   @Test(expected = InvalidArgumentException.class)
@@ -202,7 +203,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s2.xml", "s2", false, false, SubmodelType.PATHWAY));
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", null, false, true, null));
-    converter.createModel(params);
+    converter.createModel(params, new Project());
   }
 
   @Test
@@ -216,7 +217,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s2.xml", "s2", false, false, SubmodelType.PATHWAY));
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", "", false, false, null));
-    Model model = converter.createModel(params);
+    Model model = converter.createModel(params, new Project());
 
     assertNotNull(model);
 
@@ -247,7 +248,7 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.entry(new ModelZipEntryFile("s3.xml", "s3", false, false, SubmodelType.UNKNOWN));
     params.entry(new ModelZipEntryFile("mapping.xml", "", false, false, null));
     params.entry(new ModelZipEntryFile("blabla.xml", "s3", false, false, SubmodelType.UNKNOWN));
-    converter.createModel(params);
+    converter.createModel(params, new Project());
   }
 
   @Test(expected = InvalidArgumentException.class)
@@ -258,11 +259,11 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
     params.zipFile(new ZipFile("testFiles/invalid_mapping.zip"));
     params.entry(new ModelZipEntryFile("main.xml", "main", true, false, null));
     params.entry(new ModelZipEntryFile("mapping.xml", null, false, true, null));
-    converter.createModel(params);
+    converter.createModel(params, new Project());
   }
 
   @Test
-  public void testIsIgnoredFileForMac() throws Exception {
+  public void testIsIgnoredFileForMac() {
     ComplexZipConverter converter = new ComplexZipConverter(MockConverter.class);
     assertTrue(converter.isIgnoredFile("__MACOSX/.desc"));
     assertTrue(converter.isIgnoredFile(".DS_Store"));
@@ -272,13 +273,13 @@ public class ComplexZipConverterTest extends ConverterTestFunctions {
   }
 
   @Test
-  public void testIsIgnoredFileForOldMacEntries() throws Exception {
+  public void testIsIgnoredFileForOldMacEntries() {
     ComplexZipConverter converter = new ComplexZipConverter(MockConverter.class);
     assertTrue(converter.isIgnoredFile(".DS_Store/.desc"));
   }
 
   @Test
-  public void testIsIgnoredFileForValidFiles() throws Exception {
+  public void testIsIgnoredFileForValidFiles() {
     ComplexZipConverter converter = new ComplexZipConverter(MockConverter.class);
     assertFalse(converter.isIgnoredFile("mapping.xml"));
   }
diff --git a/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java b/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java
index 02ddc58b20..1485ed9620 100644
--- a/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java
+++ b/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java
@@ -1,8 +1,16 @@
 package lcsb.mapviewer.converter;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import lcsb.mapviewer.converter.zip.ImageZipEntryFile;
+import lcsb.mapviewer.model.map.OverviewImage;
+import lcsb.mapviewer.model.map.OverviewLink;
+import lcsb.mapviewer.model.map.OverviewModelLink;
+import lcsb.mapviewer.model.map.model.Model;
+import lcsb.mapviewer.model.map.model.ModelData;
+import lcsb.mapviewer.model.map.model.ModelFullIndexed;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
 
 import java.awt.geom.Point2D;
 import java.io.File;
@@ -16,23 +24,15 @@ import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
-import org.apache.commons.io.FileUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import lcsb.mapviewer.converter.zip.ImageZipEntryFile;
-import lcsb.mapviewer.model.map.OverviewImage;
-import lcsb.mapviewer.model.map.OverviewLink;
-import lcsb.mapviewer.model.map.OverviewModelLink;
-import lcsb.mapviewer.model.map.model.Model;
-import lcsb.mapviewer.model.map.model.ModelFullIndexed;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 public class OverviewParserTest extends ConverterTestFunctions {
 
   private static final String TEST_FILES_VALID_OVERVIEW_ZIP = "testFiles/valid_overview.zip";
   private static final String TEST_FILES_VALID_OVERVIEW_CASE_SENSITIVE_ZIP = "testFiles/valid_overview_case_sensitive.zip";
-  private OverviewParser parser = new OverviewParser();
+  private final OverviewParser parser = new OverviewParser();
 
   @Before
   public void setUp() throws Exception {
@@ -44,7 +44,7 @@ public class OverviewParserTest extends ConverterTestFunctions {
 
   @Test
   public void testParsingValidFile() throws Exception {
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
     List<ImageZipEntryFile> imageEntries = createImageEntries(TEST_FILES_VALID_OVERVIEW_ZIP);
     List<OverviewImage> result = parser.parseOverviewLinks(models, imageEntries, null,
         new ZipFile(TEST_FILES_VALID_OVERVIEW_ZIP));
@@ -66,8 +66,8 @@ public class OverviewParserTest extends ConverterTestFunctions {
     assertTrue(link instanceof OverviewModelLink);
 
     OverviewModelLink modelLink = (OverviewModelLink) link;
-    Model mainModel = models.iterator().next();
-    assertEquals(mainModel.getModelData(), modelLink.getLinkedModel());
+    ModelData mainModel = models.iterator().next();
+    assertEquals(mainModel, modelLink.getLinkedModel());
     assertEquals((Integer) 10, modelLink.getxCoord());
     assertEquals((Integer) 10, modelLink.getyCoord());
     assertEquals((Integer) 3, modelLink.getZoomLevel());
@@ -75,7 +75,7 @@ public class OverviewParserTest extends ConverterTestFunctions {
 
   @Test
   public void testParsingValidCaseSensitiveFile() throws Exception {
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
     List<ImageZipEntryFile> imageEntries = createImageEntries(TEST_FILES_VALID_OVERVIEW_CASE_SENSITIVE_ZIP);
     for (final ImageZipEntryFile imageZipEntryFile : imageEntries) {
       imageZipEntryFile.setFilename(imageZipEntryFile.getFilename().toLowerCase());
@@ -99,8 +99,8 @@ public class OverviewParserTest extends ConverterTestFunctions {
     assertTrue(link instanceof OverviewModelLink);
 
     OverviewModelLink modelLink = (OverviewModelLink) link;
-    Model mainModel = models.iterator().next();
-    assertEquals(mainModel.getModelData(), modelLink.getLinkedModel());
+    ModelData mainModel = models.iterator().next();
+    assertEquals(mainModel, modelLink.getLinkedModel());
     assertEquals((Integer) 10, modelLink.getxCoord());
     assertEquals((Integer) 10, modelLink.getyCoord());
     assertEquals((Integer) 3, modelLink.getZoomLevel());
@@ -109,8 +109,7 @@ public class OverviewParserTest extends ConverterTestFunctions {
   private List<ImageZipEntryFile> createImageEntries(final String string) throws IOException {
     List<ImageZipEntryFile> result = new ArrayList<>();
 
-    ZipFile zipFile = new ZipFile(string);
-    try {
+    try (ZipFile zipFile = new ZipFile(string)) {
       Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
@@ -119,14 +118,12 @@ public class OverviewParserTest extends ConverterTestFunctions {
         }
       }
       return result;
-    } finally {
-      zipFile.close();
     }
   }
 
   @Test
   public void testParsingValidFile2() throws Exception {
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
     String tmpDir = Files.createTempDirectory("tmp").toFile().getAbsolutePath();
 
@@ -145,7 +142,7 @@ public class OverviewParserTest extends ConverterTestFunctions {
   @Test(expected = InvalidOverviewFile.class)
   public void testParsingInvalidFile1() throws Exception {
     List<ImageZipEntryFile> imageEntries = createImageEntries("testFiles/invalid_overview_1.zip");
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
     parser.parseOverviewLinks(models, imageEntries, null, new ZipFile("testFiles/invalid_overview_1.zip"));
   }
@@ -153,7 +150,7 @@ public class OverviewParserTest extends ConverterTestFunctions {
   @Test(expected = InvalidOverviewFile.class)
   public void testParsingInvalidFile2() throws Exception {
     List<ImageZipEntryFile> imageEntries = createImageEntries("testFiles/invalid_overview_2.zip");
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
     parser.parseOverviewLinks(models, imageEntries, null, new ZipFile("testFiles/invalid_overview_2.zip"));
   }
@@ -161,16 +158,16 @@ public class OverviewParserTest extends ConverterTestFunctions {
   @Test(expected = InvalidOverviewFile.class)
   public void testParsingInvalidFile3() throws Exception {
     List<ImageZipEntryFile> imageEntries = createImageEntries("testFiles/invalid_overview_3.zip");
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
     parser.parseOverviewLinks(models, imageEntries, null, new ZipFile("testFiles/invalid_overview_3.zip"));
   }
 
-  private Set<Model> createValidTestMapModel() {
-    Set<Model> result = new HashSet<>();
+  private Set<ModelData> createValidTestMapModel() {
+    Set<ModelData> result = new HashSet<>();
     Model model = new ModelFullIndexed(null);
     model.setName("main");
-    result.add(model);
+    result.add(model.getModelData());
     return result;
   }
 
@@ -181,9 +178,9 @@ public class OverviewParserTest extends ConverterTestFunctions {
   public void testParseInvalidCoordinates() throws Exception {
     String invalidCoordinates = "test.png\t10,10 100,10 100,100 10,10\tmain.xml\t10,10\t3\n"
         + "test.png\t10,10 10,400 400,400 400,10\tmain.xml\t10,10\t4";
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
-    List<OverviewImage> images = new ArrayList<OverviewImage>();
+    List<OverviewImage> images = new ArrayList<>();
     OverviewImage oi = new OverviewImage();
     oi.setFilename("test.png");
     oi.setWidth(1000);
@@ -198,9 +195,9 @@ public class OverviewParserTest extends ConverterTestFunctions {
     String invalidCoordinates = "FILE\tPOLYGON\tLINK_TARGET\tMODEL_COORDINATES\tMODEL_ZOOM_LEVEL\tLINK_TYPE\n"
         + "test.png\t10,10 100,10 100,100 10,10\tmain.xml\t10,10\t3\tMODEL\n"
         + "test.png\t200,200 200,400 400,400 400,200\tmain.xml\t10,10\t4\tMODEL";
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
-    List<OverviewImage> images = new ArrayList<OverviewImage>();
+    List<OverviewImage> images = new ArrayList<>();
     OverviewImage oi = new OverviewImage();
     oi.setFilename("test.png");
     oi.setWidth(1000);
@@ -215,7 +212,7 @@ public class OverviewParserTest extends ConverterTestFunctions {
   @Test
   public void testParseValidComplexCoordinates() throws Exception {
     String invalidCoordinates = FileUtils.readFileToString(new File("testFiles/coordinates.txt"), "UTF-8");
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
     List<OverviewImage> images = new ArrayList<>();
     OverviewImage oi = new OverviewImage();
@@ -239,7 +236,7 @@ public class OverviewParserTest extends ConverterTestFunctions {
   @Test
   public void testParseValidComplexCoordinatesWithExtraImages() throws Exception {
     String invalidCoordinates = FileUtils.readFileToString(new File("testFiles/coordinates.txt"), "UTF-8");
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
 
     List<OverviewImage> images = new ArrayList<>();
     OverviewImage oi = new OverviewImage();
@@ -267,10 +264,10 @@ public class OverviewParserTest extends ConverterTestFunctions {
 
   @Test
   public void testJsonCoordinates() throws Exception {
-    Set<Model> models = createValidTestMapModel();
+    Set<ModelData> models = createValidTestMapModel();
     Model child = new ModelFullIndexed(null);
     child.setName("child");
-    models.add(child);
+    models.add(child.getModelData());
 
     List<ImageZipEntryFile> imageEntries = createImageEntries("testFiles/valid_overview_with_json.zip");
     List<OverviewImage> result = parser.parseOverviewLinks(models, imageEntries, null, new ZipFile("testFiles/valid_overview_with_json.zip"));
diff --git a/converter/src/test/java/lcsb/mapviewer/converter/ProjectFactoryTest.java b/converter/src/test/java/lcsb/mapviewer/converter/ProjectFactoryTest.java
index 327f984bdc..ea88c76453 100644
--- a/converter/src/test/java/lcsb/mapviewer/converter/ProjectFactoryTest.java
+++ b/converter/src/test/java/lcsb/mapviewer/converter/ProjectFactoryTest.java
@@ -149,7 +149,7 @@ public class ProjectFactoryTest extends ConverterTestFunctions {
   public void testParseGlyphsAndPutThemAsElementGlyphs() throws Exception {
     Model model = new ModelFullIndexed(null);
     final GenericProtein protein = createProtein();
-    protein.setNotes("Glyph: glyphs/g1.png");
+    protein.setNotes(ComplexZipConverter.GLYPH_PREFIX + "glyphs/g1.png");
     model.addElement(protein);
 
     MockConverter.modelToBeReturned = model;
@@ -188,7 +188,7 @@ public class ProjectFactoryTest extends ConverterTestFunctions {
     final Layer layer = new Layer();
 
     final LayerText text = new LayerText();
-    text.setNotes("Glyph: glyphs/g1.png");
+    text.setNotes(ComplexZipConverter.GLYPH_PREFIX + " glyphs/g1.png");
     layer.addLayerText(text);
     model.addLayer(layer);
 
diff --git a/model-command/src/test/java/lcsb/mapviewer/commands/ColorModelCommandTest.java b/model-command/src/test/java/lcsb/mapviewer/commands/ColorModelCommandTest.java
index 5d2fdaadc9..2a3b2cebfc 100644
--- a/model-command/src/test/java/lcsb/mapviewer/commands/ColorModelCommandTest.java
+++ b/model-command/src/test/java/lcsb/mapviewer/commands/ColorModelCommandTest.java
@@ -45,7 +45,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testAliasMatchWithInvalidElementId() throws Exception {
+  public void testAliasMatchWithInvalidElementId() {
     GenericDataOverlayEntry colorSchema = new GenericDataOverlayEntry();
     colorSchema.setName(null);
     colorSchema.setElementId("1");
@@ -65,7 +65,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testSpeciesMatchWithProteinType() throws Exception {
+  public void testSpeciesMatchWithProteinType() {
     GenericDataOverlayEntry colorSchema = new GenericDataOverlayEntry();
     colorSchema.setName("s1");
     colorSchema.addType(Protein.class);
@@ -82,7 +82,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testSpeciesMatchWithMiriamData() throws Exception {
+  public void testSpeciesMatchWithMiriamData() {
     GenericDataOverlayEntry colorSchema = new GenericDataOverlayEntry();
     colorSchema.setName("s1");
     colorSchema.addMiriamData(new MiriamData(MiriamType.HGNC_SYMBOL, "SNCA"));
@@ -105,7 +105,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testSpeciesMatchWithMiriamDataDifferentAnnotator() throws Exception {
+  public void testSpeciesMatchWithMiriamDataDifferentAnnotator() {
     GenericDataOverlayEntry colorSchema = new GenericDataOverlayEntry();
     colorSchema.setName("s1");
     colorSchema.addMiriamData(new MiriamData(MiriamType.HGNC_SYMBOL, "SNCA"));
@@ -123,7 +123,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionMatchWithProteinMiriamData() throws Exception {
+  public void testReactionMatchWithProteinMiriamData() {
     GenericDataOverlayEntry colorSchema = new GenericDataOverlayEntry();
     colorSchema.addMiriamData(new MiriamData(MiriamType.HGNC_SYMBOL, "SNCA"));
 
@@ -138,7 +138,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionMatchWithMiriamData() throws Exception {
+  public void testReactionMatchWithMiriamData() {
     GenericDataOverlayEntry colorSchema = new GenericDataOverlayEntry();
     colorSchema.addMiriamData(new MiriamData(MiriamType.PUBMED, "1234"));
 
@@ -155,7 +155,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
 
 
   @Test
-  public void testGetModifiedElements() throws Exception {
+  public void testGetModifiedElements() {
     Reaction reaction = new Reaction("re");
     reaction.addMiriamData(new MiriamData(MiriamType.PUBMED, "1234"));
 
@@ -174,7 +174,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testApplyColorToReaction() throws Exception {
+  public void testApplyColorToReaction() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -249,7 +249,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testColoring2() throws Exception {
+  public void testColoring2() {
     Model model = createSimpleModel();
     GenericProtein protein = createProteinWithLayout();
     protein.addMiriamData(new MiriamData(MiriamType.HGNC, "11138"));
@@ -280,7 +280,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testColorTheSameElementTwiceUsingDifferentSelector() throws Exception {
+  public void testColorTheSameElementTwiceUsingDifferentSelector() {
     Model model = createSimpleModel();
     GenericProtein protein = createProteinWithLayout();
     protein.setName("SNCA");
@@ -304,7 +304,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionColoring1() throws Exception {
+  public void testReactionColoring1() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -326,7 +326,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionColoring2() throws Exception {
+  public void testReactionColoring2() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -335,7 +335,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
     Reaction reaction = createReactionWithLayout(protein1, protein1);
     model.addReaction(reaction);
 
-    Collection<DataOverlayEntry> schemas = new ArrayList<DataOverlayEntry>();
+    Collection<DataOverlayEntry> schemas = new ArrayList<>();
     DataOverlayEntry schema = new GenericDataOverlayEntry();
     schema.setElementId(reaction.getIdReaction());
     schema.setColor(Color.RED);
@@ -351,7 +351,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionColoring3() throws Exception {
+  public void testReactionColoring3() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -360,7 +360,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
     Reaction reaction = createReactionWithLayout(protein1, protein1);
     model.addReaction(reaction);
 
-    Collection<DataOverlayEntry> schemas = new ArrayList<DataOverlayEntry>();
+    Collection<DataOverlayEntry> schemas = new ArrayList<>();
     DataOverlayEntry schema = new GenericDataOverlayEntry();
     schema.setElementId(reaction.getIdReaction());
     schema.setValue(-1.0);
@@ -376,7 +376,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionColoring4() throws Exception {
+  public void testReactionColoring4() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -386,7 +386,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
     reaction.addMiriamData(new MiriamData(MiriamType.PUBMED, "12345"));
     model.addReaction(reaction);
 
-    Collection<DataOverlayEntry> schemas = new ArrayList<DataOverlayEntry>();
+    Collection<DataOverlayEntry> schemas = new ArrayList<>();
     DataOverlayEntry schema = new GenericDataOverlayEntry();
     schema.addMiriamData(new MiriamData(MiriamType.PUBMED, "12345"));
     schema.setValue(-1.0);
@@ -403,7 +403,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testColoringComplexModel() throws Exception {
+  public void testColoringComplexModel() {
     Model model = createSimpleModel();
     Model model2 = createSimpleModel();
 
@@ -420,8 +420,10 @@ public class ColorModelCommandTest extends CommandTestFunctions {
     Model coloredModel2 = coloredModel.getSubmodelConnections().iterator().next().getSubmodel().getModel();
     Model coloredModel3 = coloredModel.getSubmodelByConnectionName("BLA");
 
-    assertNotEquals(coloredModel2.getElementByElementId(protein1.getElementId()).getFillColor(), model2.getElementByElementId(protein1.getElementId()).getFillColor());
-    assertNotEquals(coloredModel3.getElementByElementId(protein1.getElementId()).getFillColor(), model2.getElementByElementId(protein1.getElementId()).getFillColor());
+    assertNotEquals(coloredModel2.getElementByElementId(protein1.getElementId()).getFillColor(),
+        model2.getElementByElementId(protein1.getElementId()).getFillColor());
+    assertNotEquals(coloredModel3.getElementByElementId(protein1.getElementId()).getFillColor(),
+        model2.getElementByElementId(protein1.getElementId()).getFillColor());
   }
 
   @Test
@@ -453,7 +455,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testColoredReactions() throws Exception {
+  public void testColoredReactions() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -477,7 +479,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testColoredReactions2() throws Exception {
+  public void testColoredReactions2() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -506,7 +508,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionColoringWithModelNotMatching() throws Exception {
+  public void testReactionColoringWithModelNotMatching() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -529,7 +531,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testReactionColoringWithModelMatch() throws Exception {
+  public void testReactionColoringWithModelMatch() {
     Model model = createSimpleModel();
     Protein protein1 = createProteinWithLayout();
     Protein protein2 = createProteinWithLayout();
@@ -552,7 +554,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testAliasColoringWithModelNotMatching() throws Exception {
+  public void testAliasColoringWithModelNotMatching() {
     Model model = createSimpleModel();
     Protein p1 = createProtein();
     p1.setName("CNC");
@@ -571,7 +573,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testAliasColoringWithModelMatch() throws Exception {
+  public void testAliasColoringWithModelMatch() {
     Model model = createSimpleModel();
     Protein p1 = createProtein();
     p1.setName("CNC");
@@ -606,7 +608,7 @@ public class ColorModelCommandTest extends CommandTestFunctions {
   }
 
   @Test
-  public void testAliasColoringWithElementIdMatch() throws Exception {
+  public void testAliasColoringWithElementIdMatch() {
     Model model = createSimpleModel();
     model.addElement(createProtein());
 
diff --git a/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java b/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java
index b1862ab796..776c2556b7 100644
--- a/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java
+++ b/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java
@@ -3,23 +3,29 @@ package lcsb.mapviewer.model;
 import lcsb.mapviewer.common.Comparator;
 import lcsb.mapviewer.common.Configuration;
 import lcsb.mapviewer.common.comparator.SetComparator;
+import lcsb.mapviewer.model.map.layout.graphics.Glyph;
+import lcsb.mapviewer.model.map.layout.graphics.GlyphComparator;
 import lcsb.mapviewer.model.map.model.ModelComparator;
 import lcsb.mapviewer.model.map.model.ModelData;
 import lcsb.mapviewer.model.map.model.ModelDataComparator;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.util.HashSet;
+
 public class ProjectComparator extends Comparator<Project> {
 
   private static final Logger logger = LogManager.getLogger();
 
   private final ModelComparator modelComparator;
   private final SetComparator<ModelData> modelDataSetComparator;
+  private final SetComparator<Glyph> glyphSetComparator;
 
   public ProjectComparator(final double epsilon) {
     super(Project.class);
     modelComparator = new ModelComparator(epsilon);
     modelDataSetComparator = new SetComparator<>(new ModelDataComparator(epsilon));
+    glyphSetComparator = new SetComparator<>(new GlyphComparator());
   }
 
   public ProjectComparator() {
@@ -28,7 +34,13 @@ public class ProjectComparator extends Comparator<Project> {
 
   @Override
   protected int internalCompare(final Project arg0, final Project arg1) {
-    int compareResult = modelComparator.compare(arg0.getTopModel(), arg1.getTopModel());
+    int compareResult = glyphSetComparator.compare(new HashSet<>(arg0.getGlyphs()), new HashSet<>(arg1.getGlyphs()));
+    if (compareResult != 0) {
+      logger.debug("Glyphs different");
+      return compareResult;
+    }
+
+    compareResult = modelComparator.compare(arg0.getTopModel(), arg1.getTopModel());
     if (compareResult != 0) {
       logger.debug("Top model different: {}, {}", arg0.getTopModel(), arg1.getTopModel());
       return compareResult;
@@ -38,6 +50,7 @@ public class ProjectComparator extends Comparator<Project> {
       logger.debug("Models different");
       return modelDataSetComparator.compare(arg0.getModels(), arg1.getModels());
     }
+
     return 0;
   }
 
diff --git a/model/src/main/java/lcsb/mapviewer/model/cache/UploadedFileEntryComparator.java b/model/src/main/java/lcsb/mapviewer/model/cache/UploadedFileEntryComparator.java
new file mode 100644
index 0000000000..2dda81abcb
--- /dev/null
+++ b/model/src/main/java/lcsb/mapviewer/model/cache/UploadedFileEntryComparator.java
@@ -0,0 +1,32 @@
+package lcsb.mapviewer.model.cache;
+
+import lcsb.mapviewer.common.Comparator;
+import lcsb.mapviewer.common.comparator.StringComparator;
+
+import java.util.Arrays;
+
+public class UploadedFileEntryComparator extends Comparator<UploadedFileEntry> {
+
+  public UploadedFileEntryComparator() {
+    super(UploadedFileEntry.class, true);
+  }
+
+
+  @Override
+  protected int internalCompare(final UploadedFileEntry arg0, final UploadedFileEntry arg1) {
+    StringComparator stringComparator = new StringComparator();
+
+    if (stringComparator.compare(arg0.getOriginalFileName(), arg1.getOriginalFileName()) != 0) {
+      logger.debug("Original file name is different: {} != {}", arg0.getOriginalFileName(), arg1.getOriginalFileName());
+      return stringComparator.compare(arg0.getOriginalFileName(), arg1.getOriginalFileName());
+    }
+
+    if (!Arrays.equals(arg0.getFileContent(), arg1.getFileContent())) {
+      logger.debug("Glyph content different for file {}.", arg0.getOriginalFileName());
+      return -1;
+    }
+
+    return 0;
+  }
+
+}
diff --git a/model/src/main/java/lcsb/mapviewer/model/map/layout/graphics/GlyphComparator.java b/model/src/main/java/lcsb/mapviewer/model/map/layout/graphics/GlyphComparator.java
new file mode 100644
index 0000000000..e217a4bc5d
--- /dev/null
+++ b/model/src/main/java/lcsb/mapviewer/model/map/layout/graphics/GlyphComparator.java
@@ -0,0 +1,24 @@
+package lcsb.mapviewer.model.map.layout.graphics;
+
+import lcsb.mapviewer.common.Comparator;
+import lcsb.mapviewer.model.cache.UploadedFileEntryComparator;
+
+public class GlyphComparator extends Comparator<Glyph> {
+
+
+  public GlyphComparator() {
+    super(Glyph.class, true);
+  }
+
+  @Override
+  protected int internalCompare(final Glyph arg0, final Glyph arg1) {
+    UploadedFileEntryComparator fileComparator = new UploadedFileEntryComparator();
+
+    if (fileComparator.compare(arg0.getFile(), arg1.getFile()) != 0) {
+      return fileComparator.compare(arg0.getFile(), arg1.getFile());
+    }
+
+    return 0;
+  }
+
+}
diff --git a/model/src/main/java/lcsb/mapviewer/model/map/species/ElementComparator.java b/model/src/main/java/lcsb/mapviewer/model/map/species/ElementComparator.java
index 72997425c9..e4cd56e751 100644
--- a/model/src/main/java/lcsb/mapviewer/model/map/species/ElementComparator.java
+++ b/model/src/main/java/lcsb/mapviewer/model/map/species/ElementComparator.java
@@ -14,6 +14,7 @@ import lcsb.mapviewer.model.graphics.VerticalAlign;
 import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.model.map.MiriamDataComparator;
 import lcsb.mapviewer.model.map.compartment.CompartmentComparator;
+import lcsb.mapviewer.model.map.layout.graphics.GlyphComparator;
 import lcsb.mapviewer.model.map.model.ElementSubmodelConnectionComparator;
 
 /**
@@ -63,6 +64,7 @@ public class ElementComparator extends Comparator<Element> {
     StringComparator stringComparator = new StringComparator();
     ColorComparator colorComparator = new ColorComparator();
     DoubleComparator doubleComparator = new DoubleComparator(epsilon);
+    GlyphComparator glyphComparator = new GlyphComparator();
 
     if (stringComparator.compare(arg0.getElementId(), arg1.getElementId()) != 0) {
       logger.debug("ElementId different: {}, {}", arg0.getElementId(), arg1.getElementId());
@@ -216,6 +218,11 @@ public class ElementComparator extends Comparator<Element> {
       return stringComparator.compare(arg0.getImmediateLink(), arg1.getImmediateLink());
     }
 
+    if (glyphComparator.compare(arg0.getGlyph(), arg1.getGlyph()) != 0) {
+      logger.debug("Glyph different: {}, {}", arg0.getGlyph(), arg1.getGlyph());
+      return glyphComparator.compare(arg0.getGlyph(), arg1.getGlyph());
+    }
+    
     return 0;
   }
 
-- 
GitLab