Commit a125d026 authored by Daniel Gerhardt's avatar Daniel Gerhardt

Add grid image content format and corresponding migrations

The model of grid image contents has been overhauled to store data more
efficient. Fields are now represented as a single index int value
instead of a semicolon separated String. Instead of storing all
selectable fields now only the correct ones are stored.

Further changes:
* Field size instead of scale factor and zoom level for grid
* Normalized values for offsets and field size
* A single scale factor value for the image
* Rotation in degrees
* Removed coloring settings
parent 7f7af445
package de.thm.arsnova.model;
import com.fasterxml.jackson.annotation.JsonView;
import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Positive;
import de.thm.arsnova.model.serialization.View;
public class GridImageContent extends Content {
public static class Grid {
@Positive
private int columns;
@Positive
private int rows;
@Min(-1)
@Max(1)
private double normalizedX;
@Min(-1)
@Max(1)
private double normalizedY;
@Positive
@Max(1)
private double normalizedFieldSize;
private boolean visible;
@JsonView({View.Persistence.class, View.Public.class})
public int getColumns() {
return columns;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setColumns(final int columns) {
this.columns = columns;
}
@JsonView({View.Persistence.class, View.Public.class})
public int getRows() {
return rows;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setRows(final int rows) {
this.rows = rows;
}
@JsonView({View.Persistence.class, View.Public.class})
public double getNormalizedX() {
return normalizedX;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setNormalizedX(final double normalizedX) {
this.normalizedX = normalizedX;
}
@JsonView({View.Persistence.class, View.Public.class})
public double getNormalizedY() {
return normalizedY;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setNormalizedY(final double normalizedY) {
this.normalizedY = normalizedY;
}
@JsonView({View.Persistence.class, View.Public.class})
public double getNormalizedFieldSize() {
return normalizedFieldSize;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setNormalizedFieldSize(final double normalizedFieldSize) {
this.normalizedFieldSize = normalizedFieldSize;
}
@JsonView({View.Persistence.class, View.Public.class})
public boolean isVisible() {
return visible;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setVisible(final boolean visible) {
this.visible = visible;
}
}
public static class Image {
private String url;
@Min(-1)
@Max(1)
private double normalizedX;
@Min(-1)
@Max(1)
private double normalizedY;
@Positive
private double scaleFactor;
@Min(0)
@Max(360)
private int rotation;
@JsonView({View.Persistence.class, View.Public.class})
public String getUrl() {
return url;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setUrl(final String url) {
this.url = url;
}
@JsonView({View.Persistence.class, View.Public.class})
public double getNormalizedX() {
return normalizedX;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setNormalizedX(final double normalizedX) {
this.normalizedX = normalizedX;
}
@JsonView({View.Persistence.class, View.Public.class})
public double getNormalizedY() {
return normalizedY;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setNormalizedY(final double normalizedY) {
this.normalizedY = normalizedY;
}
@JsonView({View.Persistence.class, View.Public.class})
public double getScaleFactor() {
return scaleFactor;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setScaleFactor(final double scaleFactor) {
this.scaleFactor = scaleFactor;
}
@JsonView({View.Persistence.class, View.Public.class})
public int getRotation() {
return rotation;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setRotation(final int rotation) {
this.rotation = rotation;
}
}
private Grid grid;
private Image image;
private List<Integer> correctOptionIndexes = new ArrayList<>();
@JsonView({View.Persistence.class, View.Public.class})
public Grid getGrid() {
if (grid == null) {
grid = new Grid();
}
return grid;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setGrid(final Grid grid) {
this.grid = grid;
}
@JsonView({View.Persistence.class, View.Public.class})
public Image getImage() {
if (image == null) {
image = new Image();
}
return image;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setImage(final Image image) {
this.image = image;
}
/* TODO: A new JsonView is needed here. */
@JsonView(View.Persistence.class)
public List<Integer> getCorrectOptionIndexes() {
return correctOptionIndexes;
}
@JsonView({View.Persistence.class, View.Public.class})
public void setCorrectOptionIndexes(final List<Integer> correctOptionIndexes) {
this.correctOptionIndexes = correctOptionIndexes;
}
}
......@@ -31,6 +31,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import de.thm.arsnova.model.ChoiceAnswer;
import de.thm.arsnova.model.ChoiceQuestionContent;
import de.thm.arsnova.model.GridImageContent;
import de.thm.arsnova.model.TextAnswer;
import de.thm.arsnova.model.UserProfile;
import de.thm.arsnova.model.migration.v2.Answer;
......@@ -50,6 +51,7 @@ import de.thm.arsnova.model.migration.v2.RoomFeature;
* @author Daniel Gerhardt
*/
public class FromV2Migrator {
static final String V2 = "v2";
static final String V2_TYPE_ABCD = "abcd";
static final String V2_TYPE_SC = "sc";
static final String V2_TYPE_MC = "mc";
......@@ -60,6 +62,14 @@ public class FromV2Migrator {
static final String V2_TYPE_SLIDE = "slide";
static final String V2_TYPE_FLASHCARD = "flashcard";
static final String V2_TYPE_GRID = "grid";
static final String V2_GRID_DEFAULT_TYPE = "image";
static final String V2_GRID_TYPE = "gridType";
static final String V2_GRID_IMAGE_ABSOLUTE_X = "gridImageAbsoluteX";
static final String V2_GRID_IMAGE_ABSOLUTE_Y = "gridImageAbsoluteY";
static final String V2_GRID_MODERATION_DOT_LIMIT = "gridModerationDotLimit";
static final int V2_GRID_CONTAINER_SIZE = 400;
static final int V2_GRID_FIELD_COUNT = 16;
static final double V2_GRID_SCALE_FACTOR = 1.05;
private static final Map<String, de.thm.arsnova.model.Content.Format> formatMapping;
private boolean ignoreRevision = false;
......@@ -247,6 +257,54 @@ public class FromV2Migrator {
to.setAdditionalTextTitle("Back");
}
break;
case V2_TYPE_GRID:
final GridImageContent gridImageContent = new GridImageContent();
to = gridImageContent;
to.setFormat(de.thm.arsnova.model.Content.Format.GRID);
final GridImageContent.Grid grid = gridImageContent.getGrid();
grid.setColumns(from.getGridSizeX());
grid.setRows(from.getGridSizeY());
grid.setNormalizedX(1.0 * from.getGridOffsetX() / V2_GRID_CONTAINER_SIZE);
grid.setNormalizedY(1.0 * from.getGridOffsetY() / V2_GRID_CONTAINER_SIZE);
/* v3 normalized field size = v2 scale factor ^ v2 zoom level / v2 grid size */
grid.setNormalizedFieldSize(Math.pow(Double.valueOf(from.getGridScaleFactor()), from.getGridZoomLvl())
/ from.getGridSize());
grid.setVisible(!from.getGridIsHidden());
final GridImageContent.Image image = gridImageContent.getImage();
image.setUrl(from.getImage());
image.setRotation(from.getImgRotation() * 90 % 360);
image.setScaleFactor(Math.pow(Double.valueOf(from.getScaleFactor()), from.getZoomLvl()));
gridImageContent.setCorrectOptionIndexes(from.getPossibleAnswers().stream()
.filter(o -> o.isCorrect())
.map(o -> {
try {
final String[] coords = (o.getText() != null ? o.getText() : "").split(";");
return coords.length == 2
? Integer.valueOf(coords[0]) + Integer.valueOf(coords[1]) * from.getGridSizeX()
: -1;
} catch (final NumberFormatException e) {
return -1;
}
})
.filter(i -> i >= 0 && i < grid.getColumns() * grid.getRows())
.collect(Collectors.toList()));
extensions = new HashMap<>();
to.setExtensions(extensions);
v2 = new HashMap<>();
extensions.put(V2, v2);
v2.put(V2_GRID_TYPE, from.getGridType());
/* It is not possible to migrate legacy image offsets to normalized values. */
if (from.getOffsetX() != 0) {
v2.put(V2_GRID_IMAGE_ABSOLUTE_X, from.getOffsetX());
}
if (from.getOffsetY() != 0) {
v2.put(V2_GRID_IMAGE_ABSOLUTE_Y, from.getOffsetY());
}
if (from.getNumberOfDots() != 0) {
v2.put(V2_GRID_MODERATION_DOT_LIMIT, from.getNumberOfDots());
}
break;
default:
throw new IllegalArgumentException("Unsupported content format.");
......
......@@ -18,6 +18,15 @@
package de.thm.arsnova.model.migration;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_CONTAINER_SIZE;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_DEFAULT_TYPE;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_FIELD_COUNT;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_IMAGE_ABSOLUTE_X;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_IMAGE_ABSOLUTE_Y;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_MODERATION_DOT_LIMIT;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_SCALE_FACTOR;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_GRID_TYPE;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_TYPE_ABCD;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_TYPE_FLASHCARD;
import static de.thm.arsnova.model.migration.FromV2Migrator.V2_TYPE_FREETEXT;
......@@ -38,6 +47,7 @@ import java.util.stream.Collectors;
import de.thm.arsnova.model.AnswerStatistics;
import de.thm.arsnova.model.ChoiceQuestionContent;
import de.thm.arsnova.model.GridImageContent;
import de.thm.arsnova.model.RoomStatistics;
import de.thm.arsnova.model.UserProfile;
import de.thm.arsnova.model.migration.v2.Answer;
......@@ -233,6 +243,51 @@ public class ToV2Migrator {
to.setQuestionType(V2_TYPE_FREETEXT);
break;
}
break;
case GRID:
final GridImageContent fromGridImageContent = (GridImageContent) from;
final GridImageContent.Grid grid = fromGridImageContent.getGrid();
final GridImageContent.Image image = fromGridImageContent.getImage();
to.setQuestionType(V2_TYPE_GRID);
to.setGridSizeX(grid.getColumns());
to.setGridSizeY(grid.getRows());
to.setGridOffsetX((int) (Math.round(grid.getNormalizedX() * V2_GRID_CONTAINER_SIZE)));
to.setGridOffsetY((int) (Math.round(grid.getNormalizedY() * V2_GRID_CONTAINER_SIZE)));
/* v3 normalized field size = v2 scale factor ^ v2 zoom level / v2 grid size */
to.setGridSize(V2_GRID_FIELD_COUNT);
to.setGridScaleFactor(String.valueOf(V2_GRID_SCALE_FACTOR));
to.setGridZoomLvl((int) Math.round(
Math.log(grid.getNormalizedFieldSize() * V2_GRID_FIELD_COUNT)
/ Math.log(V2_GRID_SCALE_FACTOR)));
to.setGridIsHidden(!grid.isVisible());
to.setImage(image.getUrl());
to.setImgRotation(image.getRotation() / 90 % 4);
to.setScaleFactor(String.valueOf(V2_GRID_SCALE_FACTOR));
to.setZoomLvl((int) Math.round(
Math.log(image.getScaleFactor()) / Math.log(V2_GRID_SCALE_FACTOR)));
to.setPossibleAnswers(
fromGridImageContent.getCorrectOptionIndexes().stream()
.map(i -> {
final int x = i % fromGridImageContent.getGrid().getColumns();
final int y = i / fromGridImageContent.getGrid().getColumns();
final AnswerOption answerOption = new AnswerOption();
answerOption.setText(x + ";" + y);
answerOption.setCorrect(true);
return answerOption;
})
.collect(Collectors.toList()));
if (fromGridImageContent.getExtensions() != null) {
final Map<String, Object> v2 = fromGridImageContent.getExtensions()
.getOrDefault(V2, Collections.emptyMap());
to.setGridType((String) v2.getOrDefault(V2_GRID_TYPE, V2_GRID_DEFAULT_TYPE));
to.setOffsetX((int) v2.getOrDefault(V2_GRID_IMAGE_ABSOLUTE_X, 0));
to.setOffsetY((int) v2.getOrDefault(V2_GRID_IMAGE_ABSOLUTE_Y, 0));
to.setNumberOfDots((int) v2.getOrDefault(V2_GRID_MODERATION_DOT_LIMIT, 0));
} else {
to.setGridType(V2_GRID_DEFAULT_TYPE);
}
break;
default:
throw new IllegalArgumentException("Unsupported content format.");
......
......@@ -27,6 +27,7 @@ import java.io.IOException;
import de.thm.arsnova.model.ChoiceQuestionContent;
import de.thm.arsnova.model.Content;
import de.thm.arsnova.model.GridImageContent;
public class FormatContentTypeIdResolver extends TypeIdResolverBase {
@Override
......@@ -57,6 +58,8 @@ public class FormatContentTypeIdResolver extends TypeIdResolverBase {
return TypeFactory.defaultInstance().constructType(ChoiceQuestionContent.class);
case TEXT:
return TypeFactory.defaultInstance().constructType(Content.class);
case GRID:
return TypeFactory.defaultInstance().constructType(GridImageContent.class);
default:
throw new IllegalArgumentException("Unsupported type ID.");
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment