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