blob: 2ad726e20368d9c0f485dad233dbcac253ba4d39 [file] [log] [blame]
/*********************************************************************
* Copyright (c) 2008 The University of York.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipse.epsilon.flexmi;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.epsilon.flexmi.templates.Template;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
public class AttributeStructuralFeatureAllocator {
public static Map<EClass, StringSimilarityProvider> eClass2StringSimilarityProvider = new HashMap<>();
protected StringSimilarityProvider stringSimilarityProvider;
public AttributeStructuralFeatureAllocator() {
stringSimilarityProvider = new CachedStringSimilarityProvider(new DefaultStringSimilarityProvider());
}
public AttributeStructuralFeatureAllocator(EClass eclass) {
stringSimilarityProvider = eClass2StringSimilarityProvider.get(eclass);
if (stringSimilarityProvider == null) {
stringSimilarityProvider = new CachedStringSimilarityProvider(new DefaultStringSimilarityProvider());
eClass2StringSimilarityProvider.put(eclass, stringSimilarityProvider);
}
}
public Map<Node, EStructuralFeature> allocate(NamedNodeMap attributes, List<EStructuralFeature> structuralFeatures) {
final int attrLen = attributes.getLength();
Map<String, String> attributeNamesMap = new HashMap<>();
for (int i = 0; i < attrLen; i++) {
// use only the latest apparition of an attribute (expression or not)
String nodeName = attributes.item(i).getNodeName();
attributeNamesMap.put(removePrefix(nodeName), nodeName);
}
List<String> attributeNames = new ArrayList<>(attributeNamesMap.values());
List<String> structuralFeatureNames = new ArrayList<>(structuralFeatures.size());
for (EStructuralFeature feature : structuralFeatures) {
structuralFeatureNames.add(feature.getName());
}
Map<String, String> nameAllocation = allocate(attributeNames, structuralFeatureNames);
Map<Node, EStructuralFeature> allocation = new HashMap<>();
for (Map.Entry<String, String> entry : nameAllocation.entrySet()) {
String structuralFeatureName = entry.getValue();
EStructuralFeature feature = null;
for (EStructuralFeature f : structuralFeatures) {
if (structuralFeatureName.equals(f.getName())) {
feature = f;
break;
}
}
allocation.put(attributes.getNamedItem(entry.getKey()), feature);
}
return allocation;
}
public Map<String, String> allocate(List<String> values, List<String> slots) {
HashMap<String, String> result = new HashMap<>();
for (String value : new ArrayList<>(values)) {
String slot = slots.stream().filter(s -> s.equalsIgnoreCase(removePrefix(value))).findFirst().orElse(null);
if (slot != null) {
values.remove(value);
slots.remove(slot);
result.put(value, slot);
}
}
if (!values.isEmpty()) {
int[] bestSimilarity = new HungarianAllocator(values, slots).getResult();
for (int value = 0; value < values.size(); value++) {
if (bestSimilarity[value] < slots.size()) {
result.put(values.get(value), slots.get(bestSimilarity[value]));
}
}
}
return result;
}
protected String removePrefix(String str) {
if (str.startsWith(Template.PREFIX)) {
str = str.substring(Template.PREFIX.length());
}
return str;
}
/**
* Class implementing the Hungarian algorithm for attribute to feature allocation
*
* Adapted from https://github.com/amirbawab/Hungarian-Algorithm
*/
public class HungarianAllocator {
private int[][] originalValues; // Given values
private int[][] values; // Cloned given values to be processed
private int[][] lines; // Line drawn
private int numLines; // Number of line drawn
int rows[]; // Index of the column selected by every row (The final result)
int occupiedCols[]; // Verify that all column are occupied, used in the optimization step
public HungarianAllocator(List<String> attributeValues, List<String> slots) {
int matrixSize = Math.max(attributeValues.size(), slots.size());
originalValues = new int[matrixSize][matrixSize];
for (int value = 0; value < matrixSize; value++) {
for (int slot = 0; slot < matrixSize; slot++) {
if (value >= attributeValues.size() || slot >= slots.size()) {
originalValues[value][slot] = 0;
}
else {
// the "-1" allows maximising the similarity
originalValues[value][slot] = -1 * stringSimilarityProvider.getSimilarity(
slots.get(slot), removePrefix(attributeValues.get(value)));
}
}
}
values = cloneMatrix(originalValues); // Cloned matrix to be processed
rows = new int[values.length];
occupiedCols = new int[values.length];
//Algorithm
subtractRowMinimal(); // Step 1
subtractColMinimal(); // Step 2
coverZeros(); // Step 3
while (numLines < values.length) {
createAdditionalZeros(); // Step 4 (Condition)
coverZeros(); // Step 3 Again (Condition)
}
optimization(); // Optimization
}
/**
* Step 1
* Subtract from every element the minimum value from its row
*/
public void subtractRowMinimal() {
int rowMinValue[] = new int[values.length];
//get the minimum for each row and store in rowMinValue[]
for (int row = 0; row < values.length; row++) {
rowMinValue[row] = values[row][0];
for (int col = 1; col < values.length; col++) {
if (values[row][col] < rowMinValue[row])
rowMinValue[row] = values[row][col];
}
}
//subtract minimum from each row using rowMinValue[]
for (int row = 0; row < values.length; row++) {
for (int col = 0; col < values.length; col++) {
values[row][col] -= rowMinValue[row];
}
}
} //End Step 1
/**
* Step 2
* Subtract from every element the minimum value from its column
*/
public void subtractColMinimal() {
int colMinValue[] = new int[values.length];
//get the minimum for each column and store them in colMinValue[]
for (int col = 0; col < values.length; col++) {
colMinValue[col] = values[0][col];
for (int row = 1; row < values.length; row++) {
if (values[row][col] < colMinValue[col])
colMinValue[col] = values[row][col];
}
}
//subtract minimum from each column using colMinValue[]
for (int col = 0; col < values.length; col++) {
for (int row = 0; row < values.length; row++) {
values[row][col] -= colMinValue[col];
}
}
} //End Step 2
/**
* Step 3.1
* Loop through all elements, and run colorNeighbors when the element visited is equal to zero
*/
public void coverZeros() {
numLines = 0;
lines = new int[values.length][values.length];
for (int row = 0; row < values.length; row++) {
for (int col = 0; col < values.length; col++) {
if (values[row][col] == 0)
colorNeighbors(row, col, maxVH(row, col));
}
}
}
/**
* Step 3.2
* Checks which direction (vertical,horizontal) contains more zeros, every time a zero is found vertically, we increment the result
* and every time a zero is found horizontally, we decrement the result. At the end, result will be negative, zero or positive
*
* @param row Row index for the target cell
* @param col Column index for the target cell
* @return Positive integer means that the line passing by indexes [row][col] should be vertical, Zero or Negative means that the line
* passing by indexes [row][col] should be horizontal
*/
private int maxVH(int row, int col) {
int result = 0;
for (int i = 0; i < values.length; i++) {
if (values[i][col] == 0)
result++;
if (values[row][i] == 0)
result--;
}
return result;
}
/**
* Step 3.3
* Color the neighbors of the cell at index [row][col]. To know which direction to draw the lines, we pass maxVH value.
*
* @param row Row index for the target cell
* @param col Column index for the target cell
* @param maxVH Value return by the maxVH method, positive means the line to draw passing by indexes [row][col] is vertical, negative or
* zero means the line to draw passing by indexes [row][col] is horizontal
*/
private void colorNeighbors(int row, int col, int maxVH) {
if (lines[row][col] == 2)
// if cell is colored twice before (intersection cell), don't color it again
return;
if (maxVH > 0 && lines[row][col] == 1)
// if cell colored vertically and needs to be recolored vertically, don't color it again
// (Allowing this step, will color the same line (result won't change),
// but the num of line will be incremented (wrong value for the num of line drawn))
return;
if (maxVH <= 0 && lines[row][col] == -1)
// if cell colored horizontally and needs to be recolored horizontally, don't color it again
// (Allowing this step, will color the same line (result won't change),
// but the num of line will be incremented (wrong value for the num of line drawn))
return;
for (int i = 0; i < values.length; i++) { // Loop on cell at indexes [row][col] and its neighbors
if (maxVH > 0) // if value of maxVH is positive, color vertically
// if cell was colored before as horizontal (-1), and now needs to be colored vertical (1), so this cell is an intersection (2).
// Else if this value was not colored before, color it vertically
lines[i][col] = lines[i][col] == -1 || lines[i][col] == 2 ? 2 : 1;
else // if value of maxVH is zero or negative color horizontally
// if cell was colored before as vertical (1), and now needs to be colored horizontal (-1), so this cell is an intersection (2).
// Else if this value was not colored before, color it horizontally
lines[row][i] = lines[row][i] == 1 || lines[row][i] == 2 ? 2 : -1;
}
// increment line number
numLines++;
// printMatrix(lines); // Monitor the line draw steps
}//End step 3
/**
* Step 4
* This step is not always executed. (Check the algorithm in the constructor)
* Create additional zeros, by coloring the minimum value of uncovered cells (cells not colored by any line)
*/
public void createAdditionalZeros() {
// We don't know the value of the first uncovered cell, so we put a joker value 0
// (0 is safe, because before this step, all zeros were covered)
int minUncoveredValue = 0;
// Find the min in the uncovered numbers
for (int row = 0; row < values.length; row++) {
for (int col = 0; col < values.length; col++) {
if (lines[row][col] == 0 && (values[row][col] < minUncoveredValue || minUncoveredValue == 0))
minUncoveredValue = values[row][col];
}
}
// Subtract min form all uncovered elements, and add it to all elements covered twice
for (int row = 0; row < values.length; row++) {
for (int col = 0; col < values.length; col++) {
if (lines[row][col] == 0) // If uncovered, subtract
values[row][col] -= minUncoveredValue;
else if (lines[row][col] == 2) // If covered twice, add
values[row][col] += minUncoveredValue;
}
}
} // End step 4
/**
* Optimization, assign every row a cell in a unique column. Since a row can contain more than one zero,
* we need to make sure that all rows are assigned one cell from one unique column. To do this, use brute force
*
* @param row
* @param boolean If all rows were assigned a cell from a unique column, return true (at the end, guarantee true)
* @return true
*/
private boolean optimization(int row) {
if (row == rows.length) // If all rows were assigned a cell
return true;
for (int col = 0; col < values.length; col++) { // Try all columns
// If the current cell at column `col` has a value of zero, and the column is not reserved by a previous row
if (values[row][col] == 0 && occupiedCols[col] == 0) {
rows[row] = col; // Assign the current row the current column cell
occupiedCols[col] = 1; // Mark the column as reserved
// If the next rows were assigned successfully a cell from a unique column, return true
if (optimization(row + 1))
return true;
occupiedCols[col] = 0; // If the next rows were not able to get a cell, go back and try for the previous rows another cell from another column
}
}
return false; // If no cell were assigned for the current row, return false to go back one row to try to assign to it another cell from another column
}
/**
* Overload optimization(int row) method
*
* @return true
*/
public boolean optimization() {
return optimization(0);
} //End optimization
/**
* Get the result by returning an array containing the cell assigned for each row
*
* @return Array of rows where each array index represent the row number, and the value at each index is the column assigned to the
* corresponding row
*/
public int[] getResult() {
return rows;
}
/**
* Clone the 2D array
*
* @return A copy of the 2D array
*/
public int[][] cloneMatrix(int[][] matrix) {
int[][] tmp = new int[matrix.length][matrix.length];
for (int row = 0; row < matrix.length; row++) {
tmp[row] = matrix[row].clone();
}
return tmp;
}
}
}