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