From 80862f060f5658c512cb8ff4ac9311d5bc9f9bcb Mon Sep 17 00:00:00 2001 From: Piotr Gawron <piotr.gawron@uni.lu> Date: Thu, 28 Jun 2018 11:32:21 +0200 Subject: [PATCH] link to overview images is case insensitive --- .../mapviewer/converter/OverviewParser.java | 773 +++++++++--------- .../converter/OverviewParserTest.java | 40 + .../valid_overview_case_sensitive.zip | Bin 0 -> 6354 bytes 3 files changed, 436 insertions(+), 377 deletions(-) create mode 100644 converter/testFiles/valid_overview_case_sensitive.zip diff --git a/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java b/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java index f4d04e8f4a..d23f450a0c 100644 --- a/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java +++ b/converter/src/main/java/lcsb/mapviewer/converter/OverviewParser.java @@ -9,10 +9,12 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.imageio.ImageIO; @@ -37,404 +39,421 @@ import lcsb.mapviewer.model.map.model.Model; * */ public class OverviewParser { - /** - * Name of the file in zip archive where information about connections between - * {@link OverviewImage images} and models are stored. This file is tab - * separated file where every row contains information about single - * connection. - */ - private static final String COORDINATES_FILENAME = "coords.txt"; + /** + * Name of the file in zip archive where information about connections between + * {@link OverviewImage images} and models are stored. This file is tab + * separated file where every row contains information about single connection. + */ + private static final String COORDINATES_FILENAME = "coords.txt"; - /** - * 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 + * {@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"; + /** + * 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"; - /** - * Name of the column in {@link #COORDINATES_FILENAME} where comment about - * link is stored. - */ - private static final String COMMENT_COORDINATE_COLUMN = "COMMENT"; + /** + * Name of the column in {@link #COORDINATES_FILENAME} where comment about link + * is stored. + */ + private static final String COMMENT_COORDINATE_COLUMN = "COMMENT"; - /** - * Size of the buffer used to access data from input stream. - */ - private static final Integer BUFFER_SIZE = 1024; + /** + * Size of the buffer used to access data from input stream. + */ + private static final Integer BUFFER_SIZE = 1024; - /** - * String identifying {@link OverviewModelLink} connections. - */ - private static final String MODEL_LINK_TYPE = "MODEL"; + /** + * String identifying {@link OverviewModelLink} connections. + */ + private static final String MODEL_LINK_TYPE = "MODEL"; - /** - * String identifying {@link OverviewImageLink} connections between images. - */ - private static final String IMAGE_LINK_TYPE = "IMAGE"; + /** + * String identifying {@link OverviewImageLink} connections between images. + */ + private static final String IMAGE_LINK_TYPE = "IMAGE"; - /** - * String identifying {@link OverviewSearchLink} connections. - */ - private static final String SEARCH_LINK_TYPE = "SEARCH"; + /** + * String identifying {@link OverviewSearchLink} connections. + */ + private static final String SEARCH_LINK_TYPE = "SEARCH"; - /** - * Default class logger. - */ - private final Logger logger = Logger.getLogger(OverviewParser.class); + /** + * Default class logger. + */ + private final Logger logger = Logger.getLogger(OverviewParser.class); - /** - * 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 - * @return list of {@link OverviewImage images} - * @throws InvalidOverviewFile - * thrown when the zip file contains invalid data - */ - public List<OverviewImage> parseOverviewLinks(Set<Model> models, List<ImageZipEntryFile> files, String outputDirectory, ZipFile zipFile) throws InvalidOverviewFile { - if (outputDirectory != null) { - File f = new File(outputDirectory); - if (!f.exists()) { - logger.info("Directory \"" + outputDirectory + "\" doesn't exist. Creating..."); - if (!f.mkdirs()) { - throw new InvalidArgumentException("Problem with crating directory: " + outputDirectory); - } - } - } - List<OverviewImage> result = new ArrayList<OverviewImage>(); + /** + * 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 + * @return list of {@link OverviewImage images} + * @throws InvalidOverviewFile + * thrown when the zip file contains invalid data + */ + public List<OverviewImage> parseOverviewLinks(Set<Model> models, List<ImageZipEntryFile> files, + String outputDirectory, ZipFile zipFile) throws InvalidOverviewFile { + if (outputDirectory != null) { + File f = new File(outputDirectory); + if (!f.exists()) { + logger.info("Directory \"" + outputDirectory + "\" doesn't exist. Creating..."); + if (!f.mkdirs()) { + throw new InvalidArgumentException("Problem with crating directory: " + outputDirectory); + } + } + } + List<OverviewImage> result = new ArrayList<>(); - String coordinates = null; + Map<String, ZipEntry> zipEntriesByLowerCaseName = new HashMap<>(); - try { - for (ImageZipEntryFile entry : files) { - String filename = FilenameUtils.getName(entry.getFilename()); - // process image - if (filename.toLowerCase().endsWith("png")) { - OverviewImage oi = new OverviewImage(); - oi.setFilename(filename); + Enumeration<? extends ZipEntry> entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory()) { + zipEntriesByLowerCaseName.put(entry.getName().toLowerCase(), entry); + } + } - File imageFile = null; - // copy file to file system - if (outputDirectory != null) { - imageFile = new File(outputDirectory + "/" + filename); - } else { // or temporary file - imageFile = File.createTempFile("temp-file-name", ".png"); - imageFile.deleteOnExit(); - } - FileOutputStream fos = new FileOutputStream(imageFile); - byte[] bytes = new byte[BUFFER_SIZE]; - int length; - InputStream is = zipFile.getInputStream(zipFile.getEntry(entry.getFilename())); - while ((length = is.read(bytes)) >= 0) { - fos.write(bytes, 0, length); - } - fos.close(); + String coordinates = null; - // read info about image - BufferedImage bimg = ImageIO.read(imageFile); - oi.setWidth(bimg.getWidth()); - oi.setHeight(bimg.getHeight()); - result.add(oi); + try { + for (ImageZipEntryFile entry : files) { + String filename = FilenameUtils.getName(entry.getFilename()); + // process image + if (filename.toLowerCase().endsWith("png")) { + OverviewImage oi = new OverviewImage(); + oi.setFilename(filename); - // store coordinates file - } else if (filename.equalsIgnoreCase(COORDINATES_FILENAME)) { - StringBuilder sb = new StringBuilder(""); - byte[] buffer = new byte[BUFFER_SIZE]; - int read = 0; - InputStream is = zipFile.getInputStream(zipFile.getEntry(entry.getFilename())); - while ((read = is.read(buffer)) >= 0) { - sb.append(new String(buffer, 0, read)); - } - coordinates = sb.toString(); - } else { - throw new InvalidOverviewFile("Unknown file in overview images zip archive: " + filename); - } - } - } catch (IOException e) { - throw new InvalidOverviewFile("Problem with overview images file", e); - } - if (coordinates == null) { - throw new InvalidOverviewFile("File with coordinates (\"" + COORDINATES_FILENAME + "\") doesn't exist in overview images zip archive."); - } + File imageFile = null; + // copy file to file system + if (outputDirectory != null) { + imageFile = new File(outputDirectory + "/" + filename); + } else { // or temporary file + imageFile = File.createTempFile("temp-file-name", ".png"); + imageFile.deleteOnExit(); + } + FileOutputStream fos = new FileOutputStream(imageFile); + byte[] bytes = new byte[BUFFER_SIZE]; + int length; + InputStream is = zipFile.getInputStream(zipEntriesByLowerCaseName.get(entry.getFilename().toLowerCase())); + while ((length = is.read(bytes)) >= 0) { + fos.write(bytes, 0, length); + } + fos.close(); - processCoordinates(models, result, coordinates); - return result; - } + // read info about image + BufferedImage bimg = ImageIO.read(imageFile); + oi.setWidth(bimg.getWidth()); + oi.setHeight(bimg.getHeight()); + result.add(oi); - private Map<String, Model> createMapping(Set<Model> models) { - Map<String, Model> result = new HashMap<>(); - for (Model model : models) { - result.put(model.getName().toLowerCase(), model); - } - return result; - } + // store coordinates file + } else if (filename.equalsIgnoreCase(COORDINATES_FILENAME)) { + StringBuilder sb = new StringBuilder(""); + byte[] buffer = new byte[BUFFER_SIZE]; + int read = 0; + InputStream is = zipFile.getInputStream(zipEntriesByLowerCaseName.get(entry.getFilename().toLowerCase())); + while ((read = is.read(buffer)) >= 0) { + sb.append(new String(buffer, 0, read)); + } + coordinates = sb.toString(); + } else { + throw new InvalidOverviewFile("Unknown file in overview images zip archive: " + filename); + } + } + } catch (IOException e) { + throw new InvalidOverviewFile("Problem with overview images file", e); + } + if (coordinates == null) { + throw new InvalidOverviewFile( + "File with coordinates (\"" + COORDINATES_FILENAME + "\") doesn't exist in overview images zip archive."); + } - /** - * 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 - */ - protected void processCoordinates(Set<Model> models, List<OverviewImage> images, String coordinatesData) throws InvalidOverviewFile { - Map<String, Model> modelMapping = createMapping(models); - String[] rows = coordinatesData.replaceAll("\r", "\n").split("\n"); - Integer filenameColumn = null; - Integer polygonColumn = null; - Integer targetFilenameColumn = null; - Integer targetTypeColumn = null; - Integer redirectionCoordinatesColumn = null; - Integer zoomLevelColumn = null; - Integer commentColumn = null; - String[] columns = {}; - int headerLine = -1; - for (int i = 0; i < rows.length; i++) { - if (!rows[i].startsWith("#") && (!rows[i].isEmpty())) { - columns = rows[i].split("\t", -1); - headerLine = i; - break; - } - } - int headerColumns = columns.length; - for (int i = 0; i < columns.length; i++) { - String string = columns[i].trim(); - if (string.equals(ZOOM_LEVEL_COORDINATES_COLUMN)) { - if (zoomLevelColumn != null) { - throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); - } - zoomLevelColumn = i; - } else if (string.equals(REDIRECTION_COORDINATES_COORDINATE_COLUMN)) { - if (redirectionCoordinatesColumn != null) { - throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); - } - redirectionCoordinatesColumn = i; - } else if (string.equals(TARGET_FILENAME_COORDINATE_COLUMN)) { - if (targetFilenameColumn != null) { - throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); - } - targetFilenameColumn = i; - } else if (string.equals(POLYGON_COORDINATE_COLUMN)) { - if (polygonColumn != null) { - throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); - } - polygonColumn = i; - } else if (string.equals(FILENAME_COORDINATE_COLUMN)) { - if (filenameColumn != null) { - throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); - } - filenameColumn = i; - } else if (string.equals(TARGET_TYPE_COORDINATE_COLUMN)) { - if (targetTypeColumn != null) { - throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); - } - targetTypeColumn = i; - } else if (string.equals(COMMENT_COORDINATE_COLUMN)) { - if (commentColumn != null) { - throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); - } - commentColumn = i; - } else { - throw new InvalidCoordinatesFile("Unknown column name: \"" + string + "\"."); - } - } + processCoordinates(models, result, coordinates); + return result; + } - if (filenameColumn == null) { - throw new InvalidCoordinatesFile("Column \"" + FILENAME_COORDINATE_COLUMN + "\" is not defined, but is required."); - } - if (polygonColumn == null) { - throw new InvalidCoordinatesFile("Column \"" + POLYGON_COORDINATE_COLUMN + "\" is not defined, but is required."); - } - if (targetFilenameColumn == null) { - throw new InvalidCoordinatesFile("Column \"" + TARGET_FILENAME_COORDINATE_COLUMN + "\" is not defined, but is required."); - } - if (targetTypeColumn == null) { - throw new InvalidCoordinatesFile("Column \"" + TARGET_TYPE_COORDINATE_COLUMN + "\" is not defined, but is required."); - } - if (redirectionCoordinatesColumn == null) { - throw new InvalidCoordinatesFile("Column \"" + REDIRECTION_COORDINATES_COORDINATE_COLUMN + "\" is not defined, but is required."); - } - if (zoomLevelColumn == null) { - throw new InvalidCoordinatesFile("Column \"" + ZOOM_LEVEL_COORDINATES_COLUMN + "\" is not defined, but is required."); - } + private Map<String, Model> createMapping(Set<Model> models) { + Map<String, Model> result = new HashMap<>(); + for (Model model : models) { + result.put(model.getName().toLowerCase(), model); + } + return result; + } - for (int i = headerLine + 1; i < rows.length; i++) { - String row = rows[i]; - if (!row.isEmpty() && !row.startsWith("#")) { - columns = row.split("\t", -1); - if (columns.length != headerColumns) { - throw new InvalidCoordinatesFile( - "Invalid number of columns (" + columns.length + " found, but " + headerColumns + " expected). Row: \"" + row + "\""); - } - String filename = columns[filenameColumn]; - String polygon = columns[polygonColumn]; - String modelName = FilenameUtils.removeExtension(columns[targetFilenameColumn]); - String coord = columns[redirectionCoordinatesColumn]; - String zoomLevel = columns[zoomLevelColumn]; - String linkType = columns[targetTypeColumn]; - createOverviewLink(filename, polygon, modelName, coord, zoomLevel, linkType, images, modelMapping); - } - } - for (OverviewImage image : images) { - for (int i = 0; i < image.getLinks().size(); i++) { - for (int j = i + 1; j < image.getLinks().size(); j++) { - OverviewLink ol = image.getLinks().get(i); - OverviewLink ol2 = image.getLinks().get(j); + /** + * 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 + */ + protected void processCoordinates(Set<Model> models, List<OverviewImage> images, String coordinatesData) + throws InvalidOverviewFile { + Map<String, Model> modelMapping = createMapping(models); + String[] rows = coordinatesData.replaceAll("\r", "\n").split("\n"); + Integer filenameColumn = null; + Integer polygonColumn = null; + Integer targetFilenameColumn = null; + Integer targetTypeColumn = null; + Integer redirectionCoordinatesColumn = null; + Integer zoomLevelColumn = null; + Integer commentColumn = null; + String[] columns = {}; + int headerLine = -1; + for (int i = 0; i < rows.length; i++) { + if (!rows[i].startsWith("#") && (!rows[i].isEmpty())) { + columns = rows[i].split("\t", -1); + headerLine = i; + break; + } + } + int headerColumns = columns.length; + for (int i = 0; i < columns.length; i++) { + String string = columns[i].trim(); + if (string.equals(ZOOM_LEVEL_COORDINATES_COLUMN)) { + if (zoomLevelColumn != null) { + throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); + } + zoomLevelColumn = i; + } else if (string.equals(REDIRECTION_COORDINATES_COORDINATE_COLUMN)) { + if (redirectionCoordinatesColumn != null) { + throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); + } + redirectionCoordinatesColumn = i; + } else if (string.equals(TARGET_FILENAME_COORDINATE_COLUMN)) { + if (targetFilenameColumn != null) { + throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); + } + targetFilenameColumn = i; + } else if (string.equals(POLYGON_COORDINATE_COLUMN)) { + if (polygonColumn != null) { + throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); + } + polygonColumn = i; + } else if (string.equals(FILENAME_COORDINATE_COLUMN)) { + if (filenameColumn != null) { + throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); + } + filenameColumn = i; + } else if (string.equals(TARGET_TYPE_COORDINATE_COLUMN)) { + if (targetTypeColumn != null) { + throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); + } + targetTypeColumn = i; + } else if (string.equals(COMMENT_COORDINATE_COLUMN)) { + if (commentColumn != null) { + throw new InvalidCoordinatesFile("Column \"" + string + "\" is redefined."); + } + commentColumn = i; + } else { + throw new InvalidCoordinatesFile("Unknown column name: \"" + string + "\"."); + } + } - Polygon polygon = new Polygon(); - for (Point2D point : ol.getPolygonCoordinates()) { - polygon.addPoint((int) point.getX(), (int) point.getY()); - } - Polygon polygon2 = new Polygon(); - for (Point2D point : ol2.getPolygonCoordinates()) { - polygon2.addPoint((int) point.getX(), (int) point.getY()); - } - Area area = new Area(polygon); - Area area2 = new Area(polygon2); - area.intersect(area2); - if (!area.isEmpty()) { - throw new InvalidOverviewFile( - "Polygon coordinates in " + COORDINATES_FILENAME + " file overlap. Image: " + image.getFilename() + "; polgyon1 = " + ol.getPolygon() - + "; polygon2 = " + ol2.getPolygon()); - } - } - } - } + if (filenameColumn == null) { + throw new InvalidCoordinatesFile( + "Column \"" + FILENAME_COORDINATE_COLUMN + "\" is not defined, but is required."); + } + if (polygonColumn == null) { + throw new InvalidCoordinatesFile("Column \"" + POLYGON_COORDINATE_COLUMN + "\" is not defined, but is required."); + } + if (targetFilenameColumn == null) { + throw new InvalidCoordinatesFile( + "Column \"" + TARGET_FILENAME_COORDINATE_COLUMN + "\" is not defined, but is required."); + } + if (targetTypeColumn == null) { + throw new InvalidCoordinatesFile( + "Column \"" + TARGET_TYPE_COORDINATE_COLUMN + "\" is not defined, but is required."); + } + if (redirectionCoordinatesColumn == null) { + throw new InvalidCoordinatesFile( + "Column \"" + REDIRECTION_COORDINATES_COORDINATE_COLUMN + "\" is not defined, but is required."); + } + if (zoomLevelColumn == null) { + throw new InvalidCoordinatesFile( + "Column \"" + ZOOM_LEVEL_COORDINATES_COLUMN + "\" is not defined, but is required."); + } - } + for (int i = headerLine + 1; i < rows.length; i++) { + String row = rows[i]; + if (!row.isEmpty() && !row.startsWith("#")) { + columns = row.split("\t", -1); + if (columns.length != headerColumns) { + throw new InvalidCoordinatesFile("Invalid number of columns (" + columns.length + " found, but " + + headerColumns + " expected). Row: \"" + row + "\""); + } + String filename = columns[filenameColumn]; + String polygon = columns[polygonColumn]; + String modelName = FilenameUtils.removeExtension(columns[targetFilenameColumn]); + String coord = columns[redirectionCoordinatesColumn]; + String zoomLevel = columns[zoomLevelColumn]; + String linkType = columns[targetTypeColumn]; + createOverviewLink(filename, polygon, modelName, coord, zoomLevel, linkType, images, modelMapping); + } + } + for (OverviewImage image : images) { + for (int i = 0; i < image.getLinks().size(); i++) { + for (int j = i + 1; j < image.getLinks().size(); j++) { + OverviewLink ol = image.getLinks().get(i); + OverviewLink ol2 = image.getLinks().get(j); - /** - * 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 - */ - private void createOverviewLink(String filename, String polygon, String linkTarget, String coord, String zoomLevel, String linkType, - List<OverviewImage> images, Map<String, Model> models) throws InvalidCoordinatesFile { - OverviewImage image = null; - for (OverviewImage oi : images) { - if (oi.getFilename().equalsIgnoreCase(filename)) { - image = oi; - } - } - if (image == null) { - throw new InvalidCoordinatesFile("Unknown image filename in \"" + COORDINATES_FILENAME + "\": " + filename); - } - OverviewLink ol = null; - if (linkType.equals(MODEL_LINK_TYPE)) { - Model model = models.get(linkTarget.toLowerCase()); - if (model == null) { - throw new InvalidCoordinatesFile("Unknown model in \"" + COORDINATES_FILENAME + "\" file: " + linkTarget); - } - OverviewModelLink oml = new OverviewModelLink(); - oml.setLinkedModel(model); - try { - oml.setxCoord(Double.valueOf(coord.split(",")[0])); - oml.setyCoord(Double.valueOf(coord.split(",")[1])); - } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { - throw new InvalidCoordinatesFile("Problem with parsing coordinates in file \"" + COORDINATES_FILENAME + "\". Problematic text: \"" + coord + "\"", e); - } - try { - oml.setZoomLevel(Integer.valueOf(zoomLevel.trim())); - } catch (NumberFormatException e) { - throw new InvalidCoordinatesFile( - "Problem with parsing zoom level in file \"" + COORDINATES_FILENAME + "\". Problematic text: \"" + zoomLevel + "\"", e); - } - ol = oml; - } else if (linkType.equals(IMAGE_LINK_TYPE)) { - OverviewImage targetImage = null; - for (OverviewImage oi : images) { - if (FilenameUtils.removeExtension(oi.getFilename()).equalsIgnoreCase(linkTarget)) { - targetImage = oi; - } - } - if (targetImage == null) { - throw new InvalidCoordinatesFile("Unknown image filename in \"" + COORDINATES_FILENAME + "\": " + linkTarget); - } - OverviewImageLink oml = new OverviewImageLink(); - oml.setLinkedOverviewImage(targetImage); - ol = oml; - } else if (linkType.equals(SEARCH_LINK_TYPE)) { - OverviewSearchLink osl = new OverviewSearchLink(); - osl.setQuery(linkTarget); - ol = osl; - } else { - throw new InvalidCoordinatesFile("Unknown link in \"" + COORDINATES_FILENAME + "\" file: " + linkTarget); - } - ol.setPolygon(polygon); - for (Point2D point : ol.getPolygonCoordinates()) { - if (point.getX() > image.getWidth() || point.getY() > image.getHeight() || point.getX() < 0 || point.getY() < 0) { - throw new InvalidCoordinatesFile("Problem with parsing numbers in file \"" + COORDINATES_FILENAME + "\". Polygon coordinates outside image: " + point); - } - } - image.addLink(ol); + Polygon polygon = new Polygon(); + for (Point2D point : ol.getPolygonCoordinates()) { + polygon.addPoint((int) point.getX(), (int) point.getY()); + } + Polygon polygon2 = new Polygon(); + for (Point2D point : ol2.getPolygonCoordinates()) { + polygon2.addPoint((int) point.getX(), (int) point.getY()); + } + Area area = new Area(polygon); + Area area2 = new Area(polygon2); + area.intersect(area2); + if (!area.isEmpty()) { + throw new InvalidOverviewFile("Polygon coordinates in " + COORDINATES_FILENAME + " file overlap. Image: " + + image.getFilename() + "; polgyon1 = " + ol.getPolygon() + "; polygon2 = " + ol2.getPolygon()); + } + } + } + } - } + } + + /** + * 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 + */ + private void createOverviewLink(String filename, String polygon, String linkTarget, String coord, String zoomLevel, + String linkType, List<OverviewImage> images, Map<String, Model> models) throws InvalidCoordinatesFile { + OverviewImage image = null; + for (OverviewImage oi : images) { + if (oi.getFilename().equalsIgnoreCase(filename)) { + image = oi; + } + } + if (image == null) { + throw new InvalidCoordinatesFile("Unknown image filename in \"" + COORDINATES_FILENAME + "\": " + filename); + } + OverviewLink ol = null; + if (linkType.equals(MODEL_LINK_TYPE)) { + Model model = models.get(linkTarget.toLowerCase()); + if (model == null) { + throw new InvalidCoordinatesFile("Unknown model in \"" + COORDINATES_FILENAME + "\" file: " + linkTarget); + } + OverviewModelLink oml = new OverviewModelLink(); + oml.setLinkedModel(model); + try { + oml.setxCoord(Double.valueOf(coord.split(",")[0])); + oml.setyCoord(Double.valueOf(coord.split(",")[1])); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + throw new InvalidCoordinatesFile("Problem with parsing coordinates in file \"" + COORDINATES_FILENAME + + "\". Problematic text: \"" + coord + "\"", e); + } + try { + oml.setZoomLevel(Integer.valueOf(zoomLevel.trim())); + } catch (NumberFormatException e) { + throw new InvalidCoordinatesFile("Problem with parsing zoom level in file \"" + COORDINATES_FILENAME + + "\". Problematic text: \"" + zoomLevel + "\"", e); + } + ol = oml; + } else if (linkType.equals(IMAGE_LINK_TYPE)) { + OverviewImage targetImage = null; + for (OverviewImage oi : images) { + if (FilenameUtils.removeExtension(oi.getFilename()).equalsIgnoreCase(linkTarget)) { + targetImage = oi; + } + } + if (targetImage == null) { + throw new InvalidCoordinatesFile("Unknown image filename in \"" + COORDINATES_FILENAME + "\": " + linkTarget); + } + OverviewImageLink oml = new OverviewImageLink(); + oml.setLinkedOverviewImage(targetImage); + ol = oml; + } else if (linkType.equals(SEARCH_LINK_TYPE)) { + OverviewSearchLink osl = new OverviewSearchLink(); + osl.setQuery(linkTarget); + ol = osl; + } else { + throw new InvalidCoordinatesFile("Unknown link in \"" + COORDINATES_FILENAME + "\" file: " + linkTarget); + } + ol.setPolygon(polygon); + for (Point2D point : ol.getPolygonCoordinates()) { + if (point.getX() > image.getWidth() || point.getY() > image.getHeight() || point.getX() < 0 || point.getY() < 0) { + throw new InvalidCoordinatesFile("Problem with parsing numbers in file \"" + COORDINATES_FILENAME + + "\". Polygon coordinates outside image: " + point); + } + } + image.addLink(ol); + + } } diff --git a/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java b/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java index 373feff8d7..b548cab17e 100644 --- a/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java +++ b/converter/src/test/java/lcsb/mapviewer/converter/OverviewParserTest.java @@ -31,6 +31,7 @@ import lcsb.mapviewer.model.map.model.ModelFullIndexed; public class OverviewParserTest { 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"; Logger logger = Logger.getLogger(OverviewParserTest.class); OverviewParser parser = new OverviewParser(); @@ -78,6 +79,45 @@ public class OverviewParserTest { } } + @Test + public void testParsingValidCaseSensitiveFile() throws Exception { + try { + Set<Model> models = createValidTestMapModel(); + List<ImageZipEntryFile> imageEntries = createImageEntries(TEST_FILES_VALID_OVERVIEW_CASE_SENSITIVE_ZIP); + for (ImageZipEntryFile imageZipEntryFile : imageEntries) { + imageZipEntryFile.setFilename(imageZipEntryFile.getFilename().toLowerCase()); + } + List<OverviewImage> result = parser.parseOverviewLinks(models, imageEntries, null, + new ZipFile(TEST_FILES_VALID_OVERVIEW_CASE_SENSITIVE_ZIP)); + assertNotNull(result); + assertEquals(1, result.size()); + + OverviewImage img = result.get(0); + + assertEquals("test.png", img.getFilename()); + assertEquals((Integer) 639, img.getHeight()); + assertEquals((Integer) 963, img.getWidth()); + assertEquals(2, img.getLinks().size()); + + OverviewLink link = img.getLinks().get(0); + List<Point2D> polygon = link.getPolygonCoordinates(); + assertEquals(4, polygon.size()); + + assertTrue(link instanceof OverviewModelLink); + + OverviewModelLink mLink = (OverviewModelLink) link; + Model mainModel = models.iterator().next(); + assertEquals(mainModel.getModelData(), mLink.getLinkedModel()); + assertEquals((Integer) 10, mLink.getxCoord()); + assertEquals((Integer) 10, mLink.getyCoord()); + assertEquals((Integer) 3, mLink.getZoomLevel()); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + private List<ImageZipEntryFile> createImageEntries(String string) throws IOException { List<ImageZipEntryFile> result = new ArrayList<>(); diff --git a/converter/testFiles/valid_overview_case_sensitive.zip b/converter/testFiles/valid_overview_case_sensitive.zip new file mode 100644 index 0000000000000000000000000000000000000000..32977d2210b4fbedc5148cececaadd71313f0c53 GIT binary patch literal 6354 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 literal 0 HcmV?d00001 -- GitLab