From c0f013cfa5cd5786442aa74159be0dc33afbe4f3 Mon Sep 17 00:00:00 2001
From: Yanbing Zhao
Date: Sun, 12 Sep 2021 07:21:59 +0800
Subject: [PATCH] Add polynomial conformal mappings between actual and render
locations
---
.../teacon/signin/client/GuideMapScreen.java | 58 ++++-
.../signin/client/PolynomialMapping.java | 235 ++++++++++++++++++
.../teacon/signin/data/GuideMapManager.java | 2 +-
.../signup_guides/points/point_1.json | 3 +-
4 files changed, 285 insertions(+), 13 deletions(-)
create mode 100644 src/main/java/org/teacon/signin/client/PolynomialMapping.java
diff --git a/src/main/java/org/teacon/signin/client/GuideMapScreen.java b/src/main/java/org/teacon/signin/client/GuideMapScreen.java
index 95ebd20..e1f8e68 100644
--- a/src/main/java/org/teacon/signin/client/GuideMapScreen.java
+++ b/src/main/java/org/teacon/signin/client/GuideMapScreen.java
@@ -18,6 +18,10 @@
import net.minecraft.util.math.vector.Vector3i;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TranslationTextComponent;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.MarkerManager;
import org.teacon.signin.SignMeUp;
import org.teacon.signin.data.GuideMap;
import org.teacon.signin.data.Trigger;
@@ -32,6 +36,9 @@
@ParametersAreNonnullByDefault
public final class GuideMapScreen extends Screen {
+ private static final Logger LOGGER = LogManager.getLogger("SignMeUp");
+ private static final Marker MARKER = MarkerManager.getMarker("GuideMapScreen");
+
private static final int X_SIZE = 385;
private static final int Y_SIZE = 161;
@@ -46,7 +53,7 @@ public final class GuideMapScreen extends Screen {
private ResourceLocation selectedWaypoint;
private int ticksAfterWaypointTextureChanged = 0;
- private Deque lastWaypointTextures = Queues.newArrayDeque();
+ private final Deque lastWaypointTextures = Queues.newArrayDeque();
private boolean hasWaypointTrigger = false;
@@ -54,6 +61,8 @@ public final class GuideMapScreen extends Screen {
private final Vector3d playerLocation;
private final List waypointIds;
+ private PolynomialMapping mapping;
+
private ImageButton leftFlip, rightFlip;
private ImageButton mapTriggerPrev, mapTriggerNext;
@@ -106,20 +115,47 @@ protected void init() {
++j;
}
- // Setup Waypoints
+ // Collect Waypoints
this.waypointTriggers.clear();
int mapCanvasX = x0 + 78, mapCanvasY = y0 + 23;
+ final List waypoints = new ArrayList<>(this.waypointIds.size());
+ final List waypointIds = new ArrayList<>(this.waypointIds.size());
for (ResourceLocation wpId : this.waypointIds) {
Waypoint wp = SignMeUpClient.MANAGER.findWaypoint(wpId);
- if (wp == null) {
- continue;
+ if (wp != null) {
+ waypoints.add(wp);
+ waypointIds.add(wpId);
}
- // For map_icons.png, each icon is 4x4 pixels, so after we get the center coordinate,
- // we also need to shift left/up by 2 pixels to center the icon.
- final Vector3i center = this.map.center;
+ }
+
+ // Setup Mapping
+ final int waypointSize = waypoints.size();
+ final double[] inputX = new double[waypointSize], inputY = new double[waypointSize];
+ final double[] outputX = new double[waypointSize], outputY = new double[waypointSize];
+ final Vector3i center = this.map.center;
+ for (int i = 0; i < waypointSize; ++i) {
+ final Waypoint wp = waypoints.get(i);
+ final Vector3i actualLocation = wp.getActualLocation();
final Vector3i renderLocation = wp.getRenderLocation();
- final int wpX = Math.round(((float) (renderLocation.getX() - center.getX())) / this.map.radius * 64) + 64;
- final int wpY = Math.round(((float) (renderLocation.getZ() - center.getZ())) / this.map.radius * 64) + 64;
+ inputX[i] = actualLocation.getX() - center.getX();
+ inputY[i] = actualLocation.getZ() - center.getZ();
+ outputX[i] = renderLocation.getX() - center.getX();
+ outputY[i] = renderLocation.getZ() - center.getZ();
+ }
+ try {
+ this.mapping = new PolynomialMapping(inputX, inputY, outputX, outputY);
+ LOGGER.info(MARKER, "Current mapping for {} waypoint(s): {}", waypointSize, this.mapping);
+ } catch (IllegalArgumentException e) {
+ this.mapping = new PolynomialMapping(new double[0], new double[0], new double[0], new double[0]);
+ LOGGER.warn(MARKER, "Unable to generate mapping for the map.", e);
+ }
+
+ // Setup Waypoints
+ for (int i = 0; i < waypointSize; ++i) {
+ final Waypoint wp = waypoints.get(i);
+ final ResourceLocation wpId = waypointIds.get(i);
+ final int wpX = Math.round((float) outputX[i] / this.map.radius * 64) + 64;
+ final int wpY = Math.round((float) outputY[i] / this.map.radius * 64) + 64;
if (wpX >= 1 && wpX <= 127 && wpY >= 1 && wpY <= 127) {
// Setup Waypoints as ImageButtons
this.addButton(new ImageButton(mapCanvasX + wpX - 2, mapCanvasY + wpY - 2, 4, 4, 58, 2, 0, MAP_ICONS,
@@ -133,8 +169,8 @@ protected void init() {
}, wp.getTitle()));
// Setup trigger buttons from Waypoints
List wpTriggerIds = wp.getTriggerIds();
- for (int i = 0, j = 0; i < wpTriggerIds.size() && j < 7; ++i) {
- ResourceLocation triggerId = wpTriggerIds.get(i);
+ for (int j = 0, k = 0; k < wpTriggerIds.size() && j < 7; ++k) {
+ ResourceLocation triggerId = wpTriggerIds.get(k);
final Trigger trigger = SignMeUpClient.MANAGER.findTrigger(triggerId);
if (trigger == null) {
continue;
diff --git a/src/main/java/org/teacon/signin/client/PolynomialMapping.java b/src/main/java/org/teacon/signin/client/PolynomialMapping.java
new file mode 100644
index 0000000..702395d
--- /dev/null
+++ b/src/main/java/org/teacon/signin/client/PolynomialMapping.java
@@ -0,0 +1,235 @@
+package org.teacon.signin.client;
+
+import java.util.*;
+
+/**
+ * Polynomial conformal mapping from wn
to zn
: f(wn) = zn, 0 <= n < N
.
+ *
+ * The interpolation is based on newton polynomial:
+ *
+ * w = f(z) = [w0] + [w1, w0](z - z0) + [w2, w1, w0](z - z0)(z - z1) + ... +
+ * [wN, ..., w1, w0](z - z0)(z - z1)...(z - zN-1) + A(z - z0)(z - z1)...(z - zN)
+ *
+ * Extra condition: the highest factor A
should fit: f'(0) = 1
+ *
+ */
+public final class PolynomialMapping {
+ private final List inputParameters;
+ private final Collection outputDifferences;
+
+ public PolynomialMapping(double[] inputX, double[] inputY, double[] outputX, double[] outputY) {
+ int degree = this.ensureSize(inputX, inputY, outputX, outputY);
+
+ Complex zero = new Complex(0.0), one = new Complex(1.0);
+
+ LinkedHashSet inputs = new LinkedHashSet<>(degree);
+ for (int i = 0; i < degree; ++i) {
+ if (!inputs.add(new Complex(inputX[i], inputY[i]))) {
+ throw new IllegalArgumentException("Duplicate input for x = " + inputX[i] + " and y = " + inputY[i]);
+ }
+ }
+
+ if (degree > 0) {
+ this.inputParameters = new ArrayList<>(inputs);
+ } else {
+ this.inputParameters = new ArrayList<>();
+ this.inputParameters.add(zero);
+ }
+
+ ArrayDeque outputs = new ArrayDeque<>(degree);
+ for (int i = degree - 1; i >= 0; --i) {
+ Complex input = this.inputParameters.get(i); // z_i
+ Complex current = new Complex(outputX[i], outputY[i]); // [w_i]
+
+ for (int j = i + 1; j < degree; ++j) {
+ // current = [w_{j-1}, w_{j-2}, ..., w_i]
+ // outputs.remove() = [w_j, w_{j-1}, ..., w_{i+1}]
+ // result = [w_j, w_{j-1}, ..., w_{i+1}, w_i] = (outputs.remove() - current) / (z_j - z_i)
+ Complex result = outputs.remove().sub(current).div(this.inputParameters.get(j).sub(input));
+ outputs.offer(current);
+ current = result;
+ }
+
+ // left = [ w_i, w_{i-1}, ..., w_1, w_0 ]
+ outputs.offer(current);
+ }
+
+ if (degree > 0) {
+ Iterator currentOutput = outputs.iterator();
+ currentOutput.next(); // skip the first value since it is not needed for derivative
+
+ Complex currentDerivative = one, resultDerivative = zero;
+ Complex currentValue = zero.sub(this.inputParameters.get(0));
+
+ for (int i = 1; i < degree; ++i) {
+ Complex inputFactor = zero.sub(this.inputParameters.get(i));
+
+ resultDerivative = resultDerivative.add(currentDerivative.mul(currentOutput.next()));
+ currentDerivative = currentDerivative.mul(inputFactor).add(currentValue);
+
+ currentValue = currentValue.mul(inputFactor);
+ }
+
+ if (!zero.equals(currentDerivative)) {
+ outputs.add(one.sub(resultDerivative).div(currentDerivative)); // decide the highest factor
+ } else {
+ // one example: f(-1) = -i, f(1) = i
+ // then for every A, f(z) = -i + i(z + 1) + A(z + 1)(z - 1) makes f'(0) = i != 1
+ // so we should ensure f'(0) = 1 by adding two degrees of freedom to decide the highest factor
+ // then it becomes: f(z) = -i + i(z + 1) + (-1 + i)z(z + 1)(z - 1) = (-1 + i)z^3 + z
+ outputs.add(zero);
+ this.inputParameters.add(zero);
+ outputs.add(one.sub(resultDerivative).div(currentValue));
+ }
+
+ this.outputDifferences = outputs;
+ } else {
+ this.outputDifferences = outputs;
+ this.outputDifferences.add(zero);
+ this.outputDifferences.add(one);
+ }
+ }
+
+ private void interpolate(double[] inputX, double[] inputY, double[] outputX, double[] outputY) {
+ int size = this.ensureSize(inputX, inputY, outputX, outputY);
+ double[] dummyInputs = new double[size], dummyOutputs = new double[size];
+ interpolate(inputX, inputY, dummyInputs, dummyInputs, outputX, outputY, dummyOutputs, dummyOutputs);
+ }
+
+ private void interpolate(double[] inputX, double[] inputY, double[] inputDX, double[] inputDY,
+ double[] outputX, double[] outputY, double[] outputDX, double[] outputDY) {
+ int size = this.ensureSize(inputX, inputY, outputX, outputY);
+ int sizeDerivative = this.ensureSize(inputDX, inputDY, outputDX, outputDY);
+
+ if (size != sizeDerivative) {
+ throw new IllegalArgumentException("Mismatched value/derivative size: " + size + " != " + sizeDerivative);
+ }
+
+ Complex zero = new Complex(0.0), one = new Complex(1.0);
+
+ int degree = this.inputParameters.size();
+
+ for (int i = 0; i < size; ++i) {
+ Complex current = new Complex(inputX[i], inputY[i]);
+
+ Iterator currentOutput = this.outputDifferences.iterator();
+
+ Complex currentDerivative = one, resultDerivative = zero;
+ Complex currentValue = current.sub(this.inputParameters.get(0)), resultValue = currentOutput.next();
+
+ for (int j = 1; j < degree; ++j) {
+ Complex inputFactor = current.sub(this.inputParameters.get(i));
+
+ resultDerivative = resultDerivative.add(currentDerivative.mul(currentOutput.next()));
+ currentDerivative = currentDerivative.mul(inputFactor).add(currentValue);
+
+ resultValue = resultValue.add(currentValue.mul(currentOutput.next()));
+ currentValue = currentValue.mul(inputFactor);
+ }
+
+ resultDerivative = resultDerivative.add(currentDerivative.mul(currentOutput.next()));
+ resultDerivative = new Complex(inputDX[i], inputDY[i]).mul(resultDerivative);
+ resultValue = resultValue.add(currentValue.mul(currentOutput.next()));
+
+ outputDX[i] = resultDerivative.real;
+ outputDY[i] = resultDerivative.imag;
+ outputX[i] = resultValue.real;
+ outputY[i] = resultValue.imag;
+ }
+ }
+
+ private int ensureSize(double[] inputX, double[] inputY, double[] outputX, double[] outputY) {
+ if (inputX.length != inputY.length) {
+ throw new IllegalArgumentException("Mismatched input size: " + inputX.length + " != " + inputY.length);
+ }
+ if (outputX.length != outputY.length) {
+ throw new IllegalArgumentException("Mismatched output size: " + outputX.length + " != " + outputY.length);
+ }
+ if (inputX.length != outputX.length) {
+ throw new IllegalArgumentException("Mismatched input/output size: " + inputX.length + " != " + outputX.length);
+ }
+ return inputX.length;
+ }
+
+ @Override
+ public String toString() {
+ Complex zero = new Complex(0.0);
+
+ StringBuilder result = new StringBuilder();
+ StringBuilder current = new StringBuilder();
+
+ Iterator currentOutput = this.outputDifferences.iterator();
+
+ result.append("(").append(currentOutput.next().toString()).append(")");
+ current.append("*(#").append(zero.sub(this.inputParameters.get(0)).toSignedString()).append(")");
+
+ int degree = this.inputParameters.size();
+
+ for (int i = 1; i < degree; ++i) {
+ result.append("+(").append(currentOutput.next().toString()).append(")").append(current);
+ current.append("*(#").append(zero.sub(this.inputParameters.get(i)).toSignedString()).append(")");
+ }
+
+ return result.append("+(").append(currentOutput.next().toString()).append(")").append(current).toString();
+ }
+
+ private static final class Complex {
+ public final double real, imag;
+
+ public Complex(double real) {
+ this.real = real;
+ this.imag = 0.0;
+ }
+
+ public Complex(double real, double imag) {
+ this.real = real;
+ this.imag = imag;
+ }
+
+ public Complex add(Complex that) {
+ double real = this.real + that.real;
+ double imag = this.imag + that.imag;
+ return new Complex(real, imag);
+ }
+
+ public Complex sub(Complex that) {
+ double real = this.real - that.real;
+ double imag = this.imag - that.imag;
+ return new Complex(real, imag);
+ }
+
+ public Complex mul(Complex that) {
+ double real = this.real * that.real - this.imag * that.imag;
+ double imag = this.imag * that.real + this.real * that.imag;
+ return new Complex(real, imag);
+ }
+
+ public Complex div(Complex that) {
+ double factor = 1.0 / (that.real * that.real + that.imag * that.imag);
+ double real = factor * (this.real * that.real + this.imag * that.imag);
+ double imag = factor * (this.imag * that.real - this.real * that.imag);
+ return new Complex(real, imag);
+ }
+
+ public String toSignedString() {
+ return String.format("%+.15f%+.15fi", this.real, this.imag);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%.15f%+.15fi", this.real, this.imag);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return this == o || o instanceof Complex
+ && Double.compare(this.real, ((Complex) o).real) == 0
+ && Double.compare(this.imag, ((Complex) o).imag) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Double.hashCode(this.real) ^ Double.hashCode(this.imag);
+ }
+ }
+}
diff --git a/src/main/java/org/teacon/signin/data/GuideMapManager.java b/src/main/java/org/teacon/signin/data/GuideMapManager.java
index e0c77f1..a1cee7a 100644
--- a/src/main/java/org/teacon/signin/data/GuideMapManager.java
+++ b/src/main/java/org/teacon/signin/data/GuideMapManager.java
@@ -37,7 +37,7 @@
public final class GuideMapManager extends JsonReloadListener {
- private static final Logger LOGGER = LogManager.getLogger("SignMeIn");
+ private static final Logger LOGGER = LogManager.getLogger("SignMeUp");
private static final Marker MARKER = MarkerManager.getMarker("GuideMapManager");
private static final Gson GSON = new GsonBuilder().setLenient()
diff --git a/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json b/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json
index 5bbd406..1707945 100644
--- a/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json
+++ b/src/test/resources/data/sign_up_test/signup_guides/points/point_1.json
@@ -3,7 +3,8 @@
"title": "Test Point 1",
"selector": "@e[gamemode=creative]",
"location": {
- "actual": [ 10, 70, 10 ]
+ "actual": [ 10, 70, 10 ],
+ "render": [ 0, 70, 0 ]
},
"triggers": [
"sign_up_test:triggers/teleport",