/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2016 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.catrobat.catroid.sensing;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.util.Log;
import com.badlogic.gdx.math.Polygon;
import org.catrobat.catroid.common.Constants;
import org.catrobat.catroid.common.LookData;
import org.catrobat.catroid.utils.ImageEditing;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ar.com.hjg.pngj.PngjInputException;
public class CollisionInformation {
private static final String TAG = CollisionInformation.class.getSimpleName();
public Polygon[] collisionPolygons;
public Thread collisionPolygonCalculationThread;
private boolean isCalculationThreadCancelled = true;
private LookData lookData;
public CollisionInformation(LookData lookData) {
this.lookData = lookData;
}
public void calculate() {
isCalculationThreadCancelled = false;
CollisionPolygonCreationTask task = new CollisionPolygonCreationTask(lookData);
collisionPolygonCalculationThread = new Thread(task);
collisionPolygonCalculationThread.start();
}
public boolean isCalculationCancelled() {
return isCalculationThreadCancelled;
}
public void cancelCalculation() {
isCalculationThreadCancelled = true;
Log.i(TAG, "Collision Polygon Calculation of " + lookData.getLookName() + " cancelled!");
}
public int getNumberOfVertices() {
int size = 0;
for (Polygon polygon : collisionPolygons) {
size += polygon.getVertices().length / 2;
}
return size;
}
public void loadOrCreateCollisionPolygon() {
isCalculationThreadCancelled = false;
String path = lookData.getAbsolutePath();
if (collisionPolygons == null) {
if (!path.endsWith(".png")) {
Bitmap bitmap = BitmapFactory.decodeFile(path);
collisionPolygons = createCollisionPolygonByHitbox(bitmap);
return;
}
collisionPolygons = getCollisionPolygonFromPNGMeta(path);
if (collisionPolygons.length == 0) {
Log.i(TAG, "No Collision information from PNG file, creating new one.");
if (isCalculationThreadCancelled) {
return;
}
ArrayList<ArrayList<CollisionPolygonVertex>> boundingPolygon = createBoundingPolygonVertices(path, lookData);
if (boundingPolygon.size() == 0) {
return;
}
float epsilon = Constants.COLLISION_POLYGON_CREATION_EPSILON;
do {
if (isCalculationThreadCancelled) {
return;
}
ArrayList<Polygon> temporaryCollisionPolygons = new ArrayList<Polygon>();
for (int i = 0; i < boundingPolygon.size(); i++) {
if (isCalculationThreadCancelled) {
return;
}
ArrayList<PointF> points = getPointsFromPolygonVertices(boundingPolygon.get(i));
ArrayList<PointF> simplified = simplifyPolygon(points, 0, points.size() - 1,
epsilon);
if (pointToPointDistance(simplified.get(0), simplified.get(simplified.size() - 1)) < epsilon) {
simplified.remove(simplified.size() - 1);
}
if (simplified.size() < 3) {
continue;
}
temporaryCollisionPolygons.add(createPolygonFromPoints(simplified));
}
collisionPolygons = temporaryCollisionPolygons.toArray(new Polygon[temporaryCollisionPolygons
.size()]);
epsilon *= 1.2f;
}
while (getNumberOfVertices() > Constants.COLLISION_VERTEX_LIMIT);
if (collisionPolygons.length == 0) {
Bitmap bitmap = BitmapFactory.decodeFile(path);
collisionPolygons = createCollisionPolygonByHitbox(bitmap);
}
if (isCalculationThreadCancelled) {
return;
}
writeCollisionVerticesToPNGMeta(collisionPolygons, path);
Log.i("CollsionPolygon", "Polygon size of look " + lookData.getLookName() + ": " + getNumberOfVertices());
}
}
}
public static ArrayList<ArrayList<CollisionPolygonVertex>> createBoundingPolygonVertices(String absoluteBitmapPath,
LookData lookData) {
Bitmap bitmap = BitmapFactory.decodeFile(absoluteBitmapPath);
if (bitmap == null) {
Log.e("CollisionPolygon", "bitmap " + absoluteBitmapPath + " is null. Cannot create Collision polygon");
return new ArrayList<>();
}
Matrix matrix = new Matrix();
matrix.preScale(1.0f, -1.0f);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
boolean[][] grid = createCollisionGrid(bitmap);
if (lookData.getCollisionInformation().isCalculationThreadCancelled) {
return new ArrayList<>();
}
ArrayList<CollisionPolygonVertex> vertical = createVerticalVertices(grid, bitmap.getWidth(), bitmap.getHeight());
ArrayList<CollisionPolygonVertex> horizontal = createHorizontalVertices(grid, bitmap.getWidth(), bitmap.getHeight());
if (lookData.getCollisionInformation().isCalculationThreadCancelled) {
return new ArrayList<>();
}
ArrayList<ArrayList<CollisionPolygonVertex>> finalVertices = new ArrayList<>();
finalVertices.add(new ArrayList<CollisionPolygonVertex>());
int polygonNumber = 0;
finalVertices.get(polygonNumber).add(vertical.get(0));
vertical.remove(0);
do {
if (lookData.getCollisionInformation().isCalculationThreadCancelled) {
return new ArrayList<>();
}
CollisionPolygonVertex end = finalVertices.get(polygonNumber).get(finalVertices.get(polygonNumber)
.size() - 1);
boolean found = false;
for (int h = 0; h < horizontal.size(); h++) {
if (end.isConnected(horizontal.get(h))) {
finalVertices.get(polygonNumber).add(horizontal.get(h));
horizontal.remove(h);
found = true;
break;
}
}
if (found) {
end = finalVertices.get(polygonNumber).get(finalVertices.get(polygonNumber).size() - 1);
}
for (int v = 0; v < vertical.size(); v++) {
if (end.isConnected(vertical.get(v))) {
finalVertices.get(polygonNumber).add(vertical.get(v));
vertical.remove(v);
found = true;
break;
}
}
if (!found) {
polygonNumber++;
finalVertices.add(new ArrayList<CollisionPolygonVertex>());
finalVertices.get(polygonNumber).add(vertical.get(0));
vertical.remove(0);
}
} while (horizontal.size() > 0);
return finalVertices;
}
public static ArrayList<CollisionPolygonVertex> createHorizontalVertices(boolean[][] grid, int gridWidth, int
gridHeight) {
ArrayList<CollisionPolygonVertex> horizontal = new ArrayList<CollisionPolygonVertex>();
for (int y = 0; y < gridHeight; y++) {
for (int x = 0; x < gridWidth; x++) {
if (grid[x][y]) {
boolean topEdge = y == 0 || !grid[x][y - 1];
if (topEdge) {
boolean extendPrevious = horizontal.size() > 0
&& horizontal.get(horizontal.size() - 1).endX == x
&& horizontal.get(horizontal.size() - 1).endY == y;
boolean extendPreviousOtherSide = horizontal.size() > 1
&& horizontal.get(horizontal.size() - 2).endX == x
&& horizontal.get(horizontal.size() - 2).endY == y;
if (extendPrevious) {
horizontal.get(horizontal.size() - 1).extend(x + 1, y);
} else if (extendPreviousOtherSide) {
horizontal.get(horizontal.size() - 2).extend(x + 1,
horizontal.get(horizontal.size() - 2).endY);
} else {
horizontal.add(new CollisionPolygonVertex(x, y, x + 1, y));
}
}
boolean bottomEdge = y == gridHeight - 1 || !grid[x][y + 1];
if (bottomEdge) {
boolean extendPrevious = horizontal.size() > 0
&& horizontal.get(horizontal.size() - 1).endX == x
&& horizontal.get(horizontal.size() - 1).endY == y + 1;
boolean extendPreviousOtherSide = horizontal.size() > 1
&& horizontal.get(horizontal.size() - 2).endX == x
&& horizontal.get(horizontal.size() - 2).endY == y + 1;
if (extendPrevious) {
horizontal.get(horizontal.size() - 1).extend(x + 1, y + 1);
} else if (extendPreviousOtherSide) {
horizontal.get(horizontal.size() - 2).extend(x + 1,
horizontal.get(horizontal.size() - 2).endY);
} else {
horizontal.add(new CollisionPolygonVertex(x, y + 1, x + 1, y + 1));
}
}
}
}
}
return horizontal;
}
public static ArrayList<CollisionPolygonVertex> createVerticalVertices(boolean[][] grid, int gridWidth, int
gridHeight) {
ArrayList<CollisionPolygonVertex> vertical = new ArrayList<CollisionPolygonVertex>();
for (int x = 0; x < gridWidth; x++) {
for (int y = 0; y < gridHeight; y++) {
if (grid[x][y]) {
boolean leftEdge = x == 0 || !grid[x - 1][y];
if (leftEdge) {
boolean extendPrevious = vertical.size() > 0
&& vertical.get(vertical.size() - 1).endX == x
&& vertical.get(vertical.size() - 1).endY == y;
boolean extendPreviousOtherSide = vertical.size() > 1
&& vertical.get(vertical.size() - 2).endX == x
&& vertical.get(vertical.size() - 2).endY == y;
if (extendPrevious) {
vertical.get(vertical.size() - 1).extend(vertical.get(vertical.size() - 1).endX, y + 1);
} else if (extendPreviousOtherSide) {
vertical.get(vertical.size() - 2).extend(vertical.get(vertical.size() - 2).endX, y + 1);
} else {
vertical.add(new CollisionPolygonVertex(x, y, x, y + 1));
}
}
boolean rightEdge = x == gridWidth - 1 || !grid[x + 1][y];
if (rightEdge) {
boolean extendPrevious = vertical.size() > 0
&& vertical.get(vertical.size() - 1).endX == x + 1
&& vertical.get(vertical.size() - 1).endY == y;
boolean extendPreviousOtherSide = vertical.size() > 1
&& vertical.get(vertical.size() - 2).endX == x + 1
&& vertical.get(vertical.size() - 2).endY == y;
if (extendPrevious) {
vertical.get(vertical.size() - 1).extend(vertical.get(vertical.size() - 1).endX, y + 1);
} else if (extendPreviousOtherSide) {
vertical.get(vertical.size() - 2).extend(vertical.get(vertical.size() - 2).endX, y + 1);
} else {
vertical.add(new CollisionPolygonVertex(x + 1, y, x + 1, y + 1));
}
}
}
}
}
return vertical;
}
private static float pointToLineDistance(PointF lineStart, PointF lineEnd, PointF point) {
float normalLength = (float) Math.sqrt((lineEnd.x - lineStart.x) * (lineEnd.x - lineStart.x)
+ (lineEnd.y - lineStart.y) * (lineEnd.y - lineStart.y));
return Math.abs((point.x - lineStart.x) * (lineEnd.y - lineStart.y)
- (point.y - lineStart.y) * (lineEnd.x - lineStart.x)) / normalLength;
}
private static float pointToPointDistance(PointF p1, PointF p2) {
return (float) Math.sqrt(((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)));
}
public static ArrayList<PointF> simplifyPolygon(ArrayList<PointF> points, int start, int end, float epsilon) {
//Ramer-Douglas-Peucker Algorithm
float dmax = 0f;
int index = start;
for (int i = index + 1; i < end; ++i) {
float d = pointToLineDistance(points.get(start), points.get(end), points.get(i));
if (d > dmax) {
index = i;
dmax = d;
}
}
ArrayList<PointF> finalRes = new ArrayList<>();
if (dmax > epsilon) {
ArrayList<PointF> res1 = simplifyPolygon(points, start, index, epsilon);
ArrayList<PointF> res2 = simplifyPolygon(points, index, end, epsilon);
for (int i = 0; i < res1.size() - 1; i++) {
finalRes.add(res1.get(i));
}
for (int i = 0; i < res2.size(); i++) {
finalRes.add(res2.get(i));
}
} else {
finalRes.add(points.get(start));
finalRes.add(points.get(end));
}
return finalRes;
}
public static ArrayList<PointF> getPointsFromPolygonVertices(ArrayList<CollisionPolygonVertex> polygon) {
ArrayList<PointF> points = new ArrayList<>();
for (CollisionPolygonVertex vertex : polygon) {
points.add(vertex.getStartPoint());
}
return points;
}
public static Polygon createPolygonFromPoints(ArrayList<PointF> points) {
float[] polygonNodes = new float[points.size() * 2];
for (int node = 0; node < points.size(); node++) {
polygonNodes[node * 2] = points.get(node).x;
polygonNodes[node * 2 + 1] = points.get(node).y;
}
return new Polygon(polygonNodes);
}
public static boolean[][] createCollisionGrid(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
boolean[][] grid = new boolean[width][height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (bitmap.getPixel(x, y) != 0) {
grid[x][y] = true;
}
}
}
return grid;
}
public static void writeCollisionVerticesToPNGMeta(Polygon[] collisionPolygon,
String absolutePath) {
String metaToWrite = "";
for (Polygon polygon : collisionPolygon) {
for (int f = 0; f < polygon.getVertices().length; f++) {
metaToWrite += polygon.getVertices()[f] + ";";
}
metaToWrite = metaToWrite.substring(0, metaToWrite.length() - 1);
metaToWrite += "|";
}
if (!metaToWrite.equals("")) {
metaToWrite = metaToWrite.substring(0, metaToWrite.length() - 1);
ImageEditing.writeMetaDataStringToPNG(absolutePath, Constants.COLLISION_PNG_META_TAG_KEY, metaToWrite);
}
}
public static Polygon[] getCollisionPolygonFromPNGMeta(String absolutePath) {
String metadata;
try {
metadata = ImageEditing.readMetaDataStringFromPNG(absolutePath, Constants.COLLISION_PNG_META_TAG_KEY);
} catch (PngjInputException e) {
Log.e(TAG, "Error reading metadata from png!");
return new Polygon[0];
}
boolean isMetadataValid = checkMetaDataString(metadata);
if (!isMetadataValid) {
return new Polygon[0];
}
String[] polygonStrings = metadata.split("\\|");
Polygon[] collisionPolygon = new Polygon[polygonStrings.length];
for (int polygonString = 0; polygonString < polygonStrings.length; polygonString++) {
String[] pointStrings = polygonStrings[polygonString].split(";");
float[] points = new float[pointStrings.length];
for (int pointString = 0; pointString < pointStrings.length; pointString++) {
points[pointString] = Float.valueOf(pointStrings[pointString]);
}
collisionPolygon[polygonString] = new Polygon(points);
}
Log.i(TAG, "Loaded CollisionPolygon from " + absolutePath + " successfully!");
return collisionPolygon;
}
public static boolean checkMetaDataString(String metadata) {
if (metadata == null || metadata.equals("")) {
return false;
}
Pattern pattern = Pattern.compile(Constants.COLLISION_POLYGON_METADATA_PATTERN);
Matcher matcher = pattern.matcher(metadata);
if (matcher.find() && matcher.group().equals(metadata)) {
return true;
}
Log.e("Collision Polygon", "Invalid Metadata, creating new Polygon");
return false;
}
public static Polygon[] createCollisionPolygonByHitbox(Bitmap bitmap) {
float width = bitmap.getWidth();
float height = bitmap.getHeight();
float[] vertices = { 0f, 0f, width, 0f, width, height, 0f, height };
Polygon polygon = new Polygon(vertices);
Polygon[] polygons = new Polygon[1];
polygons[0] = polygon;
return polygons;
}
public void printDebugCollisionPolygons() {
int polygonNr = 0;
for (Polygon p : collisionPolygons) {
Log.i(TAG, "Collision Polygon " + ++polygonNr + " :\n" + Arrays.toString(p.getTransformedVertices()));
}
}
}