From 0206ddb1c57b73f75a801626e8a5d626ff35610b Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Fri, 14 Mar 2025 16:55:26 +0100
Subject: [PATCH] export data overlays

---
 .../CellDesignerTestFunctions.java            |  32 ++++
 .../model/celldesigner/ProjectExportTest.java |  52 +++++-
 .../converter/ColorSchemaWriter.java          | 167 ++++++++++++++++++
 .../mapviewer/converter/ProjectFactory.java   |  22 ++-
 .../converter/zip/ZipEntryFileFactory.java    |  48 +++--
 .../mapviewer/model/ProjectComparator.java    |  10 ++
 .../model/overlay/DataOverlayComparator.java  |  59 +++++++
 .../overlay/DataOverlayEntryComparator.java   | 107 +++++++++++
 .../modelutils/map/ClassNameComparator.java   |   7 +-
 .../modelutils/map/ElementUtils.java          |  57 +++---
 10 files changed, 492 insertions(+), 69 deletions(-)
 create mode 100644 converter/src/main/java/lcsb/mapviewer/converter/ColorSchemaWriter.java
 create mode 100644 model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayComparator.java
 create mode 100644 model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayEntryComparator.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 24e3bebbca..c6f6b59e96 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
@@ -1,6 +1,7 @@
 package lcsb.mapviewer.converter.model.celldesigner;
 
 import lcsb.mapviewer.common.exception.InvalidXmlSchemaException;
+import lcsb.mapviewer.common.geometry.ColorParser;
 import lcsb.mapviewer.common.tests.TestUtils;
 import lcsb.mapviewer.common.tests.UnitTestFailedWatcher;
 import lcsb.mapviewer.converter.ConverterParams;
@@ -38,9 +39,13 @@ import lcsb.mapviewer.model.map.species.Species;
 import lcsb.mapviewer.model.map.species.field.BindingRegion;
 import lcsb.mapviewer.model.map.species.field.Residue;
 import lcsb.mapviewer.model.map.species.field.StructuralState;
+import lcsb.mapviewer.model.overlay.DataOverlay;
+import lcsb.mapviewer.model.overlay.DataOverlayEntry;
+import lcsb.mapviewer.model.overlay.GenericDataOverlayEntry;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.junit.Rule;
+import org.mockito.internal.util.collections.Sets;
 
 import java.awt.Canvas;
 import java.awt.Color;
@@ -117,6 +122,7 @@ public abstract class CellDesignerTestFunctions extends TestUtils {
 
   protected static GenericProtein createProtein() {
     final GenericProtein protein = new GenericProtein("id" + (idCounter++));
+    protein.setName(faker.science().element());
     protein.setActivity(true);
     protein.setFontSize(4);
     protein.setStateLabel("xxx");
@@ -323,4 +329,30 @@ public abstract class CellDesignerTestFunctions extends TestUtils {
 
   }
 
+  protected DataOverlay createDataOverlay() {
+    DataOverlay dataOverlay = new DataOverlay();
+    dataOverlay.setPublic(faker.bool().bool());
+    dataOverlay.setName(faker.name().fullName());
+    dataOverlay.setDescription(faker.lorem().sentence());
+    return dataOverlay;
+  }
+
+  protected DataOverlayEntry createDataOverlayEntry() {
+    DataOverlayEntry entry = new GenericDataOverlayEntry();
+    entry.setName(faker.text().text(4).toUpperCase());
+    entry.setDescription(faker.lorem().sentence());
+    entry.setModelName(faker.witcher().monster());
+    entry.setCompartments(Sets.newSet(faker.witcher().potion(), faker.witcher().potion()));
+    entry.setElementId(faker.witcher().sign());
+    entry.setLineWidth(faker.number().randomDouble(2, 1, 3));
+    if (Math.random() < 0.5) {
+      entry.setColor(new ColorParser().parse(faker.color().hex()));
+    } else {
+      entry.setValue(faker.number().randomDouble(2, -1, 1));
+    }
+    entry.setReverseReaction(faker.bool().bool());
+    return entry;
+  }
+
+
 }
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 1f5034282d..4f29ab512c 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
@@ -14,6 +14,9 @@ import lcsb.mapviewer.model.map.model.ModelData;
 import lcsb.mapviewer.model.map.model.ModelSubmodelConnection;
 import lcsb.mapviewer.model.map.model.SubmodelType;
 import lcsb.mapviewer.model.map.species.Protein;
+import lcsb.mapviewer.model.overlay.DataOverlay;
+import lcsb.mapviewer.model.overlay.DataOverlayEntry;
+import lcsb.mapviewer.model.overlay.GenericDataOverlayEntry;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -26,6 +29,7 @@ import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 
 public class ProjectExportTest extends CellDesignerTestFunctions {
 
@@ -115,8 +119,50 @@ public class ProjectExportTest extends CellDesignerTestFunctions {
     testSerializationOverZip(project);
   }
 
+  @Test
+  public void testDataOverlay() throws Exception {
+    Project project = createProject();
+    Model model = createEmptyModel();
+    model.setWidth(260);
+    Protein protein = createProtein();
+    model.addElement(protein);
+    project.addModel(model);
+
+    DataOverlay overlay = createDataOverlay();
+    overlay.setPublic(true);
+    overlay.addEntry(createDataOverlayEntry());
+    overlay.addEntry(createDataOverlayEntry());
+
+    project.addDataOverlay(overlay);
+
+    testSerializationOverZip(project);
+  }
+
+  @Test
+  public void testPrivateDataOverlay() throws Exception {
+    Project project = createProject();
+    Model model = createEmptyModel();
+    model.setWidth(260);
+    Protein protein = createProtein();
+    model.addElement(protein);
+    project.addModel(model);
+
+    DataOverlay overlay = createDataOverlay();
+    overlay.setPublic(false);
+    DataOverlayEntry entry = new GenericDataOverlayEntry();
+    entry.setName(protein.getName());
+    entry.setValue(faker.number().randomDouble(2, -1, 1));
+
+    project.addDataOverlay(overlay);
+
+    testSerializationOverZip(project, false);
+  }
 
   private void testSerializationOverZip(final Project project) throws Exception {
+    testSerializationOverZip(project, true);
+  }
+
+  private void testSerializationOverZip(final Project project, final boolean shouldBeTheSame) throws Exception {
     File tempFile = File.createTempFile("CD-", ".zip");
     try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
       byte[] data = projectFactory.project2zip(project);
@@ -132,7 +178,11 @@ public class ProjectExportTest extends CellDesignerTestFunctions {
       clearModelFromZ(model);
     }
 
-    assertEquals(0, projectComparator.compare(project, project2));
+    if (shouldBeTheSame) {
+      assertEquals(0, projectComparator.compare(project, project2));
+    } else {
+      assertNotEquals(0, projectComparator.compare(project, project2));
+    }
   }
 
   private ComplexZipConverterParams createDefaultParams(final File tempFile) throws IOException {
diff --git a/converter/src/main/java/lcsb/mapviewer/converter/ColorSchemaWriter.java b/converter/src/main/java/lcsb/mapviewer/converter/ColorSchemaWriter.java
new file mode 100644
index 0000000000..543cf68ed1
--- /dev/null
+++ b/converter/src/main/java/lcsb/mapviewer/converter/ColorSchemaWriter.java
@@ -0,0 +1,167 @@
+package lcsb.mapviewer.converter;
+
+import lcsb.mapviewer.common.geometry.ColorParser;
+import lcsb.mapviewer.converter.zip.ZipEntryFileFactory;
+import lcsb.mapviewer.model.map.BioEntity;
+import lcsb.mapviewer.model.map.species.AntisenseRna;
+import lcsb.mapviewer.model.map.species.Complex;
+import lcsb.mapviewer.model.map.species.Degraded;
+import lcsb.mapviewer.model.map.species.Drug;
+import lcsb.mapviewer.model.map.species.Gene;
+import lcsb.mapviewer.model.map.species.Ion;
+import lcsb.mapviewer.model.map.species.Phenotype;
+import lcsb.mapviewer.model.map.species.Protein;
+import lcsb.mapviewer.model.map.species.Rna;
+import lcsb.mapviewer.model.map.species.SimpleMolecule;
+import lcsb.mapviewer.model.map.species.Unknown;
+import lcsb.mapviewer.model.overlay.DataOverlay;
+import lcsb.mapviewer.model.overlay.DataOverlayEntry;
+import lcsb.mapviewer.model.overlay.DataOverlayType;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ColorSchemaWriter {
+
+  private static final Map<String, Class<? extends BioEntity>> speciesMapping;
+
+  static {
+    speciesMapping = new HashMap<>();
+    speciesMapping.put("protein", Protein.class);
+    speciesMapping.put("gene", Gene.class);
+    speciesMapping.put("complex", Complex.class);
+    speciesMapping.put("simple_molecule", SimpleMolecule.class);
+    speciesMapping.put("ion", Ion.class);
+    speciesMapping.put("phenotype", Phenotype.class);
+    speciesMapping.put("drug", Drug.class);
+    speciesMapping.put("rna", Rna.class);
+    speciesMapping.put("antisense_rna", AntisenseRna.class);
+    speciesMapping.put("unknown", Unknown.class);
+    speciesMapping.put("degraded", Degraded.class);
+  }
+
+  /**
+   * Object that parses colors from string.
+   */
+  private final ColorParser colorParser = new ColorParser();
+
+  /**
+   * Default class logger.
+   */
+  private final Logger logger = LogManager.getLogger();
+
+  public String entriesToStringTable(final Set<DataOverlayEntry> entries) {
+    List<ColorSchemaColumn> columns = new ArrayList<>();
+    for (ColorSchemaColumn column : ColorSchemaColumn.values()) {
+      if (column.getTypes().contains(DataOverlayType.GENERIC)) {
+        columns.add(column);
+      }
+    }
+
+    StringBuilder builder = new StringBuilder();
+    builder.append(getHeader(columns));
+    for (DataOverlayEntry entry : entries) {
+      builder.append("\n").append(getRow(entry, columns));
+    }
+
+    return builder.toString();
+  }
+
+  private String getRow(final DataOverlayEntry entry, final List<ColorSchemaColumn> columns) {
+    StringBuilder row = new StringBuilder();
+    int counter = 0;
+    for (ColorSchemaColumn column : columns) {
+      if (counter > 0) {
+        row.append("\t");
+      }
+      counter++;
+      String data = "";
+      switch (column) {
+        case COLOR:
+          if (entry.getColor() != null) {
+            data = new ColorParser().colorToHtml(entry.getColor());
+          }
+          break;
+        case NAME:
+          if (entry.getName() != null) {
+            data = entry.getName();
+          }
+          break;
+        case ELEMENT_IDENTIFIER:
+          if (entry.getElementId() != null) {
+            data = entry.getElementId();
+          }
+          break;
+        case MAP_NAME:
+          if (entry.getModelName() != null) {
+            data = entry.getModelName();
+          }
+          break;
+        case LINE_WIDTH:
+          if (entry.getLineWidth() != null) {
+            data = entry.getLineWidth().toString();
+          }
+          break;
+        case VALUE:
+          if (entry.getValue() != null) {
+            data = entry.getValue().toString();
+          }
+          break;
+        case REVERSE_REACTION:
+          if (entry.getReverseReaction() != null) {
+            data = entry.getReverseReaction().toString();
+          }
+          break;
+        case COMPARTMENT:
+          data = StringUtils.join(entry.getCompartments(), ",");
+          break;
+        case DESCRIPTION:
+          if (entry.getDescription() != null) {
+            data = entry.getDescription().replace("\t", " ").replace("\n", " ");
+          }
+          break;
+        default:
+          data = "";
+          break;
+      }
+      row.append(data);
+    }
+    return row.toString();
+  }
+
+  private String getHeader(final List<ColorSchemaColumn> columns) {
+    StringBuilder header = new StringBuilder();
+    for (ColorSchemaColumn column : columns) {
+      if (header.length() > 0) {
+        header.append("\t");
+      }
+      header.append(column.toString());
+    }
+    return header.toString();
+  }
+
+  public String overlayToString(final DataOverlay overlay) {
+    String metaData = overlayMetaData(overlay);
+
+    String content = metaData + entriesToStringTable(overlay.getEntries());
+    return content;
+  }
+
+  private String overlayMetaData(final DataOverlay overlay) {
+    StringBuilder metaData = new StringBuilder();
+    if (overlay.getName() != null) {
+      metaData.append("#" + ZipEntryFileFactory.LAYOUT_HEADER_PARAM_NAME + "=" + overlay.getName() + "\n");
+    }
+    if (overlay.getDescription() != null) {
+      metaData.append("#" + ZipEntryFileFactory.LAYOUT_HEADER_PARAM_DESCRIPTION + "=" + overlay.getDescription().replace("\n", " ") + "\n");
+    }
+
+    return metaData.toString();
+  }
+}
diff --git a/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java b/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java
index 51e60b8766..ea388b84d9 100644
--- a/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java
+++ b/converter/src/main/java/lcsb/mapviewer/converter/ProjectFactory.java
@@ -6,6 +6,7 @@ import lcsb.mapviewer.converter.zip.GlyphZipEntryFile;
 import lcsb.mapviewer.converter.zip.ImageZipEntryFile;
 import lcsb.mapviewer.converter.zip.LayoutZipEntryFile;
 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.InconsistentModelException;
@@ -27,6 +28,7 @@ import lcsb.mapviewer.model.map.species.Complex;
 import lcsb.mapviewer.model.map.species.Element;
 import lcsb.mapviewer.model.map.species.GenericProtein;
 import lcsb.mapviewer.model.map.species.Species;
+import lcsb.mapviewer.model.overlay.DataOverlay;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -175,28 +177,42 @@ public class ProjectFactory {
       addModelFileToZip(project.getTopModel(), project.getTopModel().getName() + "." + converter.getFileExtensions().get(0), converter, zos);
       for (ModelData model : project.getModels()) {
         if (model != project.getTopModelData()) {
-          addModelFileToZip(model.getModel(), "submaps/" + model.getName() + "." + converter.getFileExtensions().get(0), converter, zos);
+          addModelFileToZip(model.getModel(), ZipEntryFileFactory.SUBMODEL_DIRECTORY + model.getName() + "." + converter.getFileExtensions().get(0), converter, zos);
         }
       }
       Model mapping = createMappingModel(project.getModels());
 
-      addModelFileToZip(mapping, "submaps/mapping." + converter.getFileExtensions().get(0), converter, zos);
+      addModelFileToZip(mapping, ZipEntryFileFactory.SUBMODEL_DIRECTORY + "mapping." + converter.getFileExtensions().get(0), converter, zos);
 
       for (ModelData model : project.getModels()) {
         addGlyphsToZip(model.getModel(), zos);
       }
 
+      for (DataOverlay overlay : project.getDataOverlays()) {
+        if (overlay.isPublic()) {
+          addDataOverlayToZip(overlay, zos);
+        }
+      }
+
     } catch (IOException ioe) {
       throw new ConverterException(ioe);
     }
     return byteArrayOutputStream.toByteArray();
   }
 
+  private void addDataOverlayToZip(final DataOverlay overlay, final ZipOutputStream zos) throws IOException {
+    ColorSchemaWriter writer = new ColorSchemaWriter();
+    ZipEntry entry = new ZipEntry(ZipEntryFileFactory.LAYOUT_DIRECTORY + overlay.getId() + "_" + overlay.getName());
+    zos.putNextEntry(entry);
+    zos.write(writer.overlayToString(overlay).getBytes());
+    zos.closeEntry();
+  }
+
   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());
+        ZipEntry entry = new ZipEntry(ZipEntryFileFactory.GLYPHS_DIRECTORY + new File(uploadedFileEntry.getOriginalFileName()).getName());
         zos.putNextEntry(entry);
         zos.write(uploadedFileEntry.getFileContent());
         zos.closeEntry();
diff --git a/converter/src/main/java/lcsb/mapviewer/converter/zip/ZipEntryFileFactory.java b/converter/src/main/java/lcsb/mapviewer/converter/zip/ZipEntryFileFactory.java
index 317842d763..8bc71e00b2 100644
--- a/converter/src/main/java/lcsb/mapviewer/converter/zip/ZipEntryFileFactory.java
+++ b/converter/src/main/java/lcsb/mapviewer/converter/zip/ZipEntryFileFactory.java
@@ -1,23 +1,21 @@
 package lcsb.mapviewer.converter.zip;
 
+import lcsb.mapviewer.common.TextFileUtils;
+import lcsb.mapviewer.common.exception.InvalidArgumentException;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Map;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
-import org.apache.commons.io.FilenameUtils;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import lcsb.mapviewer.common.TextFileUtils;
-import lcsb.mapviewer.common.exception.InvalidArgumentException;
-
 /**
  * Factory class used to create {@link ZipEntryFile} objects.
- * 
+ *
  * @author Piotr Gawron
- * 
  */
 public class ZipEntryFileFactory {
   /**
@@ -39,7 +37,7 @@ public class ZipEntryFileFactory {
    * Directory in a zip file where information about submodels is stored. These
    * entries should be by default transformed into {@link ModelZipEntryFile}.
    */
-  private static final String SUBMODEL_DIRECTORY = "submaps/";
+  public static final String SUBMODEL_DIRECTORY = "submaps/";
   /**
    * Directory in a zip file where information about
    * {@link lcsb.mapviewer.model.map.OverviewImage OverviewImage} is stored. These
@@ -51,24 +49,24 @@ public class ZipEntryFileFactory {
    * {@link lcsb.mapviewer.model.map.layout.graphics.Glyph} is stored. These
    * entries should be by default transformed into {@link GlyphZipEntryFile}.
    */
-  private static final String GLYPHS_DIRECTORY = "glyphs/";
+  public static final String GLYPHS_DIRECTORY = "glyphs/";
   /**
    * Directory in a zip file where information about
    * {@link lcsb.mapviewer.model.map.layout.Layout Layout} is stored. These
    * entries should be by default transformed into {@link LayoutZipEntryFile}.
    */
-  private static final String LAYOUT_DIRECTORY = "layouts/";
+  public static final String LAYOUT_DIRECTORY = "layouts/";
   /**
    * Name of the parameter in {@link LayoutZipEntryFile file describing layout}
    * corresponding to the {@link LayoutZipEntryFile#name layout name}.
    */
-  private static final String LAYOUT_HEADER_PARAM_NAME = "NAME";
+  public static final String LAYOUT_HEADER_PARAM_NAME = "NAME";
   /**
    * Name of the parameter in {@link LayoutZipEntryFile file describing layout}
    * corresponding to the {@link LayoutZipEntryFile#description layout
    * description}.
    */
-  private static final String LAYOUT_HEADER_PARAM_DESCRIPTION = "DESCRIPTION";
+  public static final String LAYOUT_HEADER_PARAM_DESCRIPTION = "DESCRIPTION";
   /**
    * Default class logger.
    */
@@ -78,14 +76,11 @@ public class ZipEntryFileFactory {
   /**
    * Generates instance of {@link ZipEntryFile} representing entry in the zip file
    * with pre-parsed structural data (like: type, name, description, etc).
-   * 
-   * @param entry
-   *          {@link ZipEntry entry} in the {@link ZipFile}
-   * @param zipFile
-   *          original {@link ZipFile}
+   *
+   * @param entry   {@link ZipEntry entry} in the {@link ZipFile}
+   * @param zipFile original {@link ZipFile}
    * @return {@link ZipEntryFile} for the given {@link ZipEntry}
-   * @throws IOException
-   *           thrown when there is a problem with accessing zip file
+   * @throws IOException thrown when there is a problem with accessing zip file
    */
   public ZipEntryFile createZipEntryFile(final ZipEntry entry, final ZipFile zipFile) throws IOException {
     if (entry.isDirectory()) {
@@ -142,14 +137,11 @@ public class ZipEntryFileFactory {
   /**
    * Creates {@link LayoutZipEntryFile layout entry} from input stream and given
    * name of the layout.
-   * 
-   * @param name
-   *          name of the layout
-   * @param inputStream
-   *          stream where data is stored
+   *
+   * @param name        name of the layout
+   * @param inputStream stream where data is stored
    * @return {@link LayoutZipEntryFile} processed from input data
-   * @throws IOException
-   *           thrown when there is problem with accessing input data
+   * @throws IOException thrown when there is problem with accessing input data
    */
   public LayoutZipEntryFile createLayoutZipEntryFile(final String name, final InputStream inputStream) throws IOException {
     LayoutZipEntryFile result = new LayoutZipEntryFile();
diff --git a/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java b/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java
index 776c2556b7..c11a06102d 100644
--- a/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java
+++ b/model/src/main/java/lcsb/mapviewer/model/ProjectComparator.java
@@ -8,6 +8,8 @@ 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 lcsb.mapviewer.model.overlay.DataOverlay;
+import lcsb.mapviewer.model.overlay.DataOverlayComparator;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -19,6 +21,7 @@ public class ProjectComparator extends Comparator<Project> {
 
   private final ModelComparator modelComparator;
   private final SetComparator<ModelData> modelDataSetComparator;
+  private final SetComparator<DataOverlay> dataOverlaySetComparator;
   private final SetComparator<Glyph> glyphSetComparator;
 
   public ProjectComparator(final double epsilon) {
@@ -26,6 +29,7 @@ public class ProjectComparator extends Comparator<Project> {
     modelComparator = new ModelComparator(epsilon);
     modelDataSetComparator = new SetComparator<>(new ModelDataComparator(epsilon));
     glyphSetComparator = new SetComparator<>(new GlyphComparator());
+    dataOverlaySetComparator = new SetComparator<>(new DataOverlayComparator(epsilon));
   }
 
   public ProjectComparator() {
@@ -51,6 +55,12 @@ public class ProjectComparator extends Comparator<Project> {
       return modelDataSetComparator.compare(arg0.getModels(), arg1.getModels());
     }
 
+    int status = dataOverlaySetComparator.compare(new HashSet<>(arg0.getDataOverlays()), new HashSet<>(arg1.getDataOverlays()));
+    if (status != 0) {
+      logger.debug("Data overlays different");
+      return status;
+    }
+
     return 0;
   }
 
diff --git a/model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayComparator.java b/model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayComparator.java
new file mode 100644
index 0000000000..5937141f60
--- /dev/null
+++ b/model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayComparator.java
@@ -0,0 +1,59 @@
+package lcsb.mapviewer.model.overlay;
+
+import lcsb.mapviewer.common.Comparator;
+import lcsb.mapviewer.common.Configuration;
+import lcsb.mapviewer.common.comparator.SetComparator;
+import lcsb.mapviewer.common.comparator.StringComparator;
+
+import java.util.HashSet;
+
+public class DataOverlayComparator extends Comparator<DataOverlay> {
+
+  private final SetComparator<DataOverlayEntry> dataOverlayEntrySetComparator;
+
+  public DataOverlayComparator(final double epsilon) {
+    super(DataOverlay.class, true);
+    this.dataOverlayEntrySetComparator = new SetComparator<>(new DataOverlayEntryComparator(epsilon));
+  }
+
+  public DataOverlayComparator() {
+    this(Configuration.EPSILON);
+  }
+
+  @Override
+  protected int internalCompare(final DataOverlay arg0, final DataOverlay arg1) {
+    StringComparator stringComparator = new StringComparator();
+
+    int status = stringComparator.compare(arg0.getName(), arg1.getName());
+    if (status != 0) {
+      logger.debug("Name different: {}, {}", arg0.getName(), arg1.getName());
+      return status;
+    }
+
+    status = stringComparator.compare(arg0.getDescription(), arg1.getDescription());
+    if (status != 0) {
+      logger.debug("Description different: {}, {}", arg0.getDescription(), arg1.getDescription());
+      return status;
+    }
+
+    status = stringComparator.compare(arg0.getGenomeVersion(), arg1.getGenomeVersion());
+    if (status != 0) {
+      logger.debug("GenomeVersion different: {}, {}", arg0.getGenomeVersion(), arg1.getGenomeVersion());
+      return status;
+    }
+
+    status = stringComparator.compare(arg0.getGenomeVersion(), arg1.getGenomeVersion());
+    if (status != 0) {
+      logger.debug("GenomeVersion different: {}, {}", arg0.getGenomeVersion(), arg1.getGenomeVersion());
+      return status;
+    }
+
+    status = dataOverlayEntrySetComparator.compare(new HashSet<>(arg0.getEntries()), new HashSet<>(arg1.getEntries()));
+    if (status != 0) {
+      logger.debug("Entries different");
+      return status;
+    }
+
+    return 0;
+  }
+}
diff --git a/model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayEntryComparator.java b/model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayEntryComparator.java
new file mode 100644
index 0000000000..8ab3b2deef
--- /dev/null
+++ b/model/src/main/java/lcsb/mapviewer/model/overlay/DataOverlayEntryComparator.java
@@ -0,0 +1,107 @@
+package lcsb.mapviewer.model.overlay;
+
+import lcsb.mapviewer.common.Comparator;
+import lcsb.mapviewer.common.Configuration;
+import lcsb.mapviewer.common.comparator.BooleanComparator;
+import lcsb.mapviewer.common.comparator.ColorComparator;
+import lcsb.mapviewer.common.comparator.DoubleComparator;
+import lcsb.mapviewer.common.comparator.SetComparator;
+import lcsb.mapviewer.common.comparator.StringComparator;
+import lcsb.mapviewer.model.map.BioEntity;
+import lcsb.mapviewer.model.map.MiriamData;
+import lcsb.mapviewer.model.map.MiriamDataComparator;
+import lcsb.mapviewer.modelutils.map.ClassNameComparator;
+
+public class DataOverlayEntryComparator extends Comparator<DataOverlayEntry> {
+
+  private final SetComparator<MiriamData> miriamDataSetComparator = new SetComparator<>(new MiriamDataComparator());
+
+  private final SetComparator<String> stringSetComparator = new SetComparator<>(new StringComparator());
+  private final SetComparator<Class<? extends BioEntity>> classSetComparator = new SetComparator<>(new ClassNameComparator<>());
+
+  private final ColorComparator colorComparator = new ColorComparator();
+  private final BooleanComparator booleanComparator = new BooleanComparator();
+  private final DoubleComparator doubleComparator;
+
+  public DataOverlayEntryComparator(final double epsilon) {
+    super(DataOverlayEntry.class);
+    doubleComparator = new DoubleComparator(epsilon);
+  }
+
+  public DataOverlayEntryComparator() {
+    this(Configuration.EPSILON);
+  }
+
+  @Override
+  protected int internalCompare(final DataOverlayEntry arg0, final DataOverlayEntry arg1) {
+    StringComparator stringComparator = new StringComparator();
+
+    int status = stringComparator.compare(arg0.getName(), arg1.getName());
+    if (status != 0) {
+      logger.debug("Name different: {}, {}", arg0.getName(), arg1.getName());
+      return status;
+    }
+
+    status = stringComparator.compare(arg0.getDescription(), arg1.getDescription());
+    if (status != 0) {
+      logger.debug("Description different: {}, {}", arg0.getDescription(), arg1.getDescription());
+      return status;
+    }
+
+    status = stringComparator.compare(arg0.getElementId(), arg1.getElementId());
+    if (status != 0) {
+      logger.debug("ElementId different: {}, {}", arg0.getElementId(), arg1.getElementId());
+      return status;
+    }
+
+    status = stringComparator.compare(arg0.getModelName(), arg1.getModelName());
+    if (status != 0) {
+      logger.debug("ModelName different: {}, {}", arg0.getModelName(), arg1.getModelName());
+      return status;
+    }
+
+    status = colorComparator.compare(arg0.getColor(), arg1.getColor());
+    if (status != 0) {
+      logger.debug("Color different: {}, {}", arg0.getColor(), arg1.getColor());
+      return status;
+    }
+
+    status = miriamDataSetComparator.compare(arg0.getMiriamData(), arg1.getMiriamData());
+    if (status != 0) {
+      logger.debug("MiriamData different: {}, {}", arg0.getMiriamData(), arg1.getMiriamData());
+      return status;
+    }
+
+    status = stringSetComparator.compare(arg0.getCompartments(), arg1.getCompartments());
+    if (status != 0) {
+      logger.debug("Compartments different: {}, {}", arg0.getCompartments(), arg1.getCompartments());
+      return status;
+    }
+
+    status = doubleComparator.compare(arg0.getLineWidth(), arg1.getLineWidth());
+    if (status != 0) {
+      logger.debug("LineWidth different: {}, {}", arg0.getLineWidth(), arg1.getLineWidth());
+      return status;
+    }
+
+    status = booleanComparator.compare(arg0.getReverseReaction(), arg1.getReverseReaction());
+    if (status != 0) {
+      logger.debug("ReverseReaction different: {}, {}", arg0.getReverseReaction(), arg1.getReverseReaction());
+      return status;
+    }
+
+    status = doubleComparator.compare(arg0.getValue(), arg1.getValue());
+    if (status != 0) {
+      logger.debug("Value different: {}, {}", arg0.getValue(), arg1.getValue());
+      return status;
+    }
+
+    status = classSetComparator.compare(arg0.getTypes(), arg1.getTypes());
+    if (status != 0) {
+      logger.debug("Types different: {}, {}", arg0.getTypes(), arg1.getTypes());
+      return status;
+    }
+
+    return 0;
+  }
+}
diff --git a/model/src/main/java/lcsb/mapviewer/modelutils/map/ClassNameComparator.java b/model/src/main/java/lcsb/mapviewer/modelutils/map/ClassNameComparator.java
index f88c513a5b..a25bcd8764 100644
--- a/model/src/main/java/lcsb/mapviewer/modelutils/map/ClassNameComparator.java
+++ b/model/src/main/java/lcsb/mapviewer/modelutils/map/ClassNameComparator.java
@@ -4,14 +4,13 @@ import java.util.Comparator;
 
 /**
  * Class used to compare classes using class names (SimpleName).
- * 
+ *
  * @author Piotr Gawron
- * 
  */
-public class ClassNameComparator implements Comparator<Class<?>> {
+public class ClassNameComparator<T> implements Comparator<Class<? extends T>> {
 
   @Override
-  public int compare(final Class<?> o1, final Class<?> o2) {
+  public int compare(final Class<? extends T> o1, final Class<? extends T> o2) {
     return o1.getSimpleName().compareTo(o2.getSimpleName());
   }
 
diff --git a/model/src/main/java/lcsb/mapviewer/modelutils/map/ElementUtils.java b/model/src/main/java/lcsb/mapviewer/modelutils/map/ElementUtils.java
index 019eb8ff2f..d47c340960 100644
--- a/model/src/main/java/lcsb/mapviewer/modelutils/map/ElementUtils.java
+++ b/model/src/main/java/lcsb/mapviewer/modelutils/map/ElementUtils.java
@@ -1,5 +1,15 @@
 package lcsb.mapviewer.modelutils.map;
 
+import lcsb.mapviewer.model.map.BioEntity;
+import lcsb.mapviewer.model.map.Drawable;
+import lcsb.mapviewer.model.map.reaction.Reaction;
+import lcsb.mapviewer.model.map.reaction.ReactionNode;
+import lcsb.mapviewer.model.map.species.Element;
+import lcsb.mapviewer.model.map.species.field.ModificationResidue;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.reflections.Reflections;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -11,22 +21,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.reflections.Reflections;
-
-import lcsb.mapviewer.model.map.BioEntity;
-import lcsb.mapviewer.model.map.Drawable;
-import lcsb.mapviewer.model.map.reaction.Reaction;
-import lcsb.mapviewer.model.map.reaction.ReactionNode;
-import lcsb.mapviewer.model.map.species.Element;
-import lcsb.mapviewer.model.map.species.field.ModificationResidue;
-
 /**
  * Class with some util method for {@link BioEntity} objects.
- * 
+ *
  * @author Piotr Gawron
- * 
  */
 public final class ElementUtils {
 
@@ -52,23 +50,21 @@ public final class ElementUtils {
   /**
    * Default class logger.
    */
-  private static Logger logger = LogManager.getLogger();
+  private static final Logger logger = LogManager.getLogger();
 
   /**
-   * @param elementClasses
-   *          the elementClasses to set
+   * @param elementClasses the elementClasses to set
    * @see #elementClasses
    */
-  protected static void setElementClasses(final Map<String, Class<? extends Element>> elementClasses) {
+  static void setElementClasses(final Map<String, Class<? extends Element>> elementClasses) {
     ElementUtils.elementClasses = elementClasses;
   }
 
   /**
-   * @param reactionClasses
-   *          the reactionClasses to set
+   * @param reactionClasses the reactionClasses to set
    * @see #reactionClasses
    */
-  protected static void setReactionClasses(final Map<String, Class<? extends Reaction>> reactionClasses) {
+  static void setReactionClasses(final Map<String, Class<? extends Reaction>> reactionClasses) {
     ElementUtils.reactionClasses = reactionClasses;
   }
 
@@ -76,8 +72,7 @@ public final class ElementUtils {
    * This method return tag that identifies {@link BioEntity}. This tag should
    * be used in warning messages.
    *
-   * @param element
-   *          tag for this element is created
+   * @param element tag for this element is created
    * @return tag that identifies element
    */
   public String getElementTag(final Drawable element) {
@@ -88,11 +83,9 @@ public final class ElementUtils {
    * This method return tag that identifies {@link BioEntity}. This tag should
    * be used in warning messages.
    *
-   * @param element
-   *          tag for this element is created
-   * @param annotator
-   *          this object identifies class that will produce warning. it can be
-   *          null (in such situation it will be skipped in the tag)
+   * @param element   tag for this element is created
+   * @param annotator this object identifies class that will produce warning. it can be
+   *                  null (in such situation it will be skipped in the tag)
    * @return tag that identifies element
    */
   public String getElementTag(final Drawable element, final Object annotator) {
@@ -182,8 +175,7 @@ public final class ElementUtils {
    * Returns list of classes that extends {@link Element} class, but don't have
    * children (leaves in the hierarchy tree).
    *
-   * @return list of classes that extends {@link Element} class, but don't have
-   *         children (leaves in the hierarchy tree)
+   * @return list of classes that extends {@link Element} class, but don't have children (leaves in the hierarchy tree)
    */
   public List<Class<? extends Element>> getAvailableElementSubclasses() {
     List<Class<? extends Element>> result = new ArrayList<>();
@@ -191,14 +183,14 @@ public final class ElementUtils {
       refreshClasses();
     }
     result.addAll(elementClasses.values());
-    Collections.sort(result, new ClassNameComparator());
+    Collections.sort(result, new ClassNameComparator<>());
     return result;
   }
 
   /**
    * Refresh list of known implementation of {@link Element} class.
    */
-  protected void refreshClasses() {
+  private void refreshClasses() {
     bioEntityClassByStringName = new HashMap<>();
 
     List<Class<? extends Element>> tmp = new ArrayList<>();
@@ -270,8 +262,7 @@ public final class ElementUtils {
   /**
    * Returns a {@link Class} that extends {@link BioEntity} for a given name.
    *
-   * @param name
-   *          name of the class
+   * @param name name of the class
    * @return {@link Class} that extends {@link BioEntity} for a given name
    */
   public Class<?> getClassByName(final String name) {
-- 
GitLab