| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.gui.util.imagery;
|
|---|
| 3 |
|
|---|
| 4 | import java.awt.Point;
|
|---|
| 5 | import java.awt.Rectangle;
|
|---|
| 6 | import java.awt.geom.Point2D;
|
|---|
| 7 | import java.awt.image.BufferedImage;
|
|---|
| 8 | import java.awt.image.DataBuffer;
|
|---|
| 9 | import java.awt.image.DataBufferByte;
|
|---|
| 10 | import java.awt.image.DataBufferDouble;
|
|---|
| 11 | import java.awt.image.DataBufferInt;
|
|---|
| 12 | import java.util.stream.IntStream;
|
|---|
| 13 |
|
|---|
| 14 | import org.openstreetmap.josm.tools.Logging;
|
|---|
| 15 |
|
|---|
| 16 | import jakarta.annotation.Nullable;
|
|---|
| 17 | import org.openstreetmap.josm.tools.bugreport.BugReport;
|
|---|
| 18 |
|
|---|
| 19 | /**
|
|---|
| 20 | * The plane that the camera appears on and rotates around.
|
|---|
| 21 | * @since 18246
|
|---|
| 22 | */
|
|---|
| 23 | public class CameraPlane {
|
|---|
| 24 | /** The field of view for the panorama at 0 zoom */
|
|---|
| 25 | static final double PANORAMA_FOV = Math.toRadians(110);
|
|---|
| 26 |
|
|---|
| 27 | /** This determines the yaw direction. We may want to make it a config option, but maybe not */
|
|---|
| 28 | private static final byte YAW_DIRECTION = -1;
|
|---|
| 29 |
|
|---|
| 30 | /** The width of the image */
|
|---|
| 31 | private final int width;
|
|---|
| 32 | /** The height of the image */
|
|---|
| 33 | private final int height;
|
|---|
| 34 |
|
|---|
| 35 | private final Vector3D[][] vectors;
|
|---|
| 36 | private Vector3D rotation;
|
|---|
| 37 |
|
|---|
| 38 | public static final double HALF_PI = Math.PI / 2;
|
|---|
| 39 | public static final double TWO_PI = 2 * Math.PI;
|
|---|
| 40 |
|
|---|
| 41 | /**
|
|---|
| 42 | * Create a new CameraPlane with the default FOV (110 degrees).
|
|---|
| 43 | *
|
|---|
| 44 | * @param width The width of the image
|
|---|
| 45 | * @param height The height of the image
|
|---|
| 46 | */
|
|---|
| 47 | public CameraPlane(int width, int height) {
|
|---|
| 48 | this(width, height, (width / 2d) / Math.tan(PANORAMA_FOV / 2));
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | /**
|
|---|
| 52 | * Create a new CameraPlane
|
|---|
| 53 | *
|
|---|
| 54 | * @param width The width of the image
|
|---|
| 55 | * @param height The height of the image
|
|---|
| 56 | * @param distance The radial distance of the photosphere
|
|---|
| 57 | */
|
|---|
| 58 | private CameraPlane(int width, int height, double distance) {
|
|---|
| 59 | this.width = width;
|
|---|
| 60 | this.height = height;
|
|---|
| 61 | this.rotation = new Vector3D(Vector3D.VectorType.RPA, distance, 0, 0);
|
|---|
| 62 | this.vectors = new Vector3D[width][height];
|
|---|
| 63 | IntStream.range(0, this.height).parallel().forEach(y -> IntStream.range(0, this.width).parallel()
|
|---|
| 64 | .forEach(x -> this.vectors[x][y] = this.getVector3D((double) x, y)));
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | /**
|
|---|
| 68 | * Get the width of the image
|
|---|
| 69 | * @return The width of the image
|
|---|
| 70 | */
|
|---|
| 71 | public int getWidth() {
|
|---|
| 72 | return this.width;
|
|---|
| 73 | }
|
|---|
| 74 |
|
|---|
| 75 | /**
|
|---|
| 76 | * Get the height of the image
|
|---|
| 77 | * @return The height of the image
|
|---|
| 78 | */
|
|---|
| 79 | public int getHeight() {
|
|---|
| 80 | return this.height;
|
|---|
| 81 | }
|
|---|
| 82 |
|
|---|
| 83 | /**
|
|---|
| 84 | * Get the point for a vector
|
|---|
| 85 | *
|
|---|
| 86 | * @param vector the vector for which the corresponding point on the camera plane will be returned
|
|---|
| 87 | * @return the point on the camera plane to which the given vector is mapped, nullable
|
|---|
| 88 | */
|
|---|
| 89 | @Nullable
|
|---|
| 90 | public Point getPoint(final Vector3D vector) {
|
|---|
| 91 | final Vector3D rotatedVector = rotate(vector);
|
|---|
| 92 | // Currently set to false due to change in painting
|
|---|
| 93 | if (rotatedVector.getZ() < 0) {
|
|---|
| 94 | // Ignores any points "behind the back", so they don't get painted a second time on the other
|
|---|
| 95 | // side of the sphere
|
|---|
| 96 | return null;
|
|---|
| 97 | }
|
|---|
| 98 | // This is a slightly faster than just doing the (brute force) method of Math.max(Math.min)). Reduces if
|
|---|
| 99 | // statements by 1 per call.
|
|---|
| 100 | final long x = Math
|
|---|
| 101 | .round((rotatedVector.getX() / rotatedVector.getZ()) * this.rotation.getRadialDistance() + width / 2d);
|
|---|
| 102 | final long y = Math
|
|---|
| 103 | .round((rotatedVector.getY() / rotatedVector.getZ()) * this.rotation.getRadialDistance() + height / 2d);
|
|---|
| 104 |
|
|---|
| 105 | try {
|
|---|
| 106 | return new Point(Math.toIntExact(x), Math.toIntExact(y));
|
|---|
| 107 | } catch (ArithmeticException e) {
|
|---|
| 108 | return new Point((int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, x)),
|
|---|
| 109 | (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, y)));
|
|---|
| 110 | }
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | /**
|
|---|
| 114 | * Convert a point to a 3D vector
|
|---|
| 115 | *
|
|---|
| 116 | * @param p The point to convert
|
|---|
| 117 | * @return The vector
|
|---|
| 118 | */
|
|---|
| 119 | public Vector3D getVector3D(final Point p) {
|
|---|
| 120 | return this.getVector3D(p.x, p.y);
|
|---|
| 121 | }
|
|---|
| 122 |
|
|---|
| 123 | /**
|
|---|
| 124 | * Convert a point to a 3D vector (vectors are cached)
|
|---|
| 125 | *
|
|---|
| 126 | * @param x The x coordinate
|
|---|
| 127 | * @param y The y coordinate
|
|---|
| 128 | * @return The vector
|
|---|
| 129 | */
|
|---|
| 130 | public Vector3D getVector3D(final int x, final int y) {
|
|---|
| 131 | Vector3D res;
|
|---|
| 132 | try {
|
|---|
| 133 | res = rotate(vectors[x][y]);
|
|---|
| 134 | } catch (Exception e) {
|
|---|
| 135 | Logging.trace(e);
|
|---|
| 136 | res = Vector3D.DEFAULT_VECTOR_3D;
|
|---|
| 137 | }
|
|---|
| 138 | return res;
|
|---|
| 139 | }
|
|---|
| 140 |
|
|---|
| 141 | /**
|
|---|
| 142 | * Convert a point to a 3D vector. Warning: This method does not cache.
|
|---|
| 143 | *
|
|---|
| 144 | * @param x The x coordinate
|
|---|
| 145 | * @param y The y coordinate
|
|---|
| 146 | * @return The vector (the middle of the image is 0, 0)
|
|---|
| 147 | */
|
|---|
| 148 | public Vector3D getVector3D(final double x, final double y) {
|
|---|
| 149 | return new Vector3D(x - width / 2d, y - height / 2d, this.rotation.getRadialDistance()).normalize();
|
|---|
| 150 | }
|
|---|
| 151 |
|
|---|
| 152 | /**
|
|---|
| 153 | * Set camera plane rotation by current plane position.
|
|---|
| 154 | *
|
|---|
| 155 | * @param p Point within current plane.
|
|---|
| 156 | */
|
|---|
| 157 | public synchronized void setRotation(final Point p) {
|
|---|
| 158 | setRotation(getVector3D(p));
|
|---|
| 159 | }
|
|---|
| 160 |
|
|---|
| 161 | /**
|
|---|
| 162 | * Set the rotation from the difference of two points
|
|---|
| 163 | *
|
|---|
| 164 | * @param from The originating point
|
|---|
| 165 | * @param to The new point
|
|---|
| 166 | */
|
|---|
| 167 | public void setRotationFromDelta(final Point from, final Point to) {
|
|---|
| 168 | // Bound check (bounds are essentially the image viewer component)
|
|---|
| 169 | if (from.x < 0 || from.y < 0 || to.x < 0 || to.y < 0
|
|---|
| 170 | || from.x > this.vectors.length - 1 || from.y > this.vectors[from.x].length - 1
|
|---|
| 171 | || to.x > this.vectors.length - 1 || to.y > this.vectors[to.x].length - 1) {
|
|---|
| 172 | return;
|
|---|
| 173 | }
|
|---|
| 174 | Vector3D f1 = this.vectors[from.x][from.y];
|
|---|
| 175 | Vector3D t1 = this.vectors[to.x][to.y];
|
|---|
| 176 | double deltaPolarAngle = f1.getPolarAngle() - t1.getPolarAngle();
|
|---|
| 177 | double deltaAzimuthalAngle = t1.getAzimuthalAngle() - f1.getAzimuthalAngle();
|
|---|
| 178 | double polarAngle = this.rotation.getPolarAngle() + deltaPolarAngle;
|
|---|
| 179 | double azimuthalAngle = this.rotation.getAzimuthalAngle() + deltaAzimuthalAngle;
|
|---|
| 180 | this.setRotation(azimuthalAngle, polarAngle);
|
|---|
| 181 | }
|
|---|
| 182 |
|
|---|
| 183 | /**
|
|---|
| 184 | * Set camera plane rotation by spherical vector.
|
|---|
| 185 | *
|
|---|
| 186 | * @param vec vector pointing new view position.
|
|---|
| 187 | */
|
|---|
| 188 | public synchronized void setRotation(Vector3D vec) {
|
|---|
| 189 | setRotation(vec.getPolarAngle(), vec.getAzimuthalAngle());
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | public synchronized Vector3D getRotation() {
|
|---|
| 193 | return this.rotation;
|
|---|
| 194 | }
|
|---|
| 195 |
|
|---|
| 196 | synchronized void setRotation(double azimuthalAngle, double polarAngle) {
|
|---|
| 197 | // Note: Something, somewhere, is switching the two.
|
|---|
| 198 | // FIXME: Figure out what is switching them and why
|
|---|
| 199 | // Prevent us from going much outside 2pi
|
|---|
| 200 | if (polarAngle < 0) {
|
|---|
| 201 | polarAngle = polarAngle + TWO_PI;
|
|---|
| 202 | } else if (polarAngle > TWO_PI) {
|
|---|
| 203 | polarAngle = polarAngle - TWO_PI;
|
|---|
| 204 | }
|
|---|
| 205 | // Avoid flipping the camera
|
|---|
| 206 | if (azimuthalAngle > HALF_PI) {
|
|---|
| 207 | azimuthalAngle = HALF_PI;
|
|---|
| 208 | } else if (azimuthalAngle < -HALF_PI) {
|
|---|
| 209 | azimuthalAngle = -HALF_PI;
|
|---|
| 210 | }
|
|---|
| 211 | this.rotation = new Vector3D(Vector3D.VectorType.RPA, this.rotation.getRadialDistance(), polarAngle, azimuthalAngle);
|
|---|
| 212 | }
|
|---|
| 213 |
|
|---|
| 214 | /**
|
|---|
| 215 | * Rotate a vector using the current rotation
|
|---|
| 216 | * @param vec The vector to rotate
|
|---|
| 217 | * @return A rotated vector
|
|---|
| 218 | */
|
|---|
| 219 | private Vector3D rotate(final Vector3D vec) {
|
|---|
| 220 | // @formatting:off
|
|---|
| 221 | /* Full rotation matrix for a yaw-pitch-roll
|
|---|
| 222 | * yaw = alpha, pitch = beta, roll = gamma (typical representations)
|
|---|
| 223 | * [cos(alpha), -sin(alpha), 0 ] [cos(beta), 0, sin(beta) ] [1, 0 , 0 ] [x] [x1]
|
|---|
| 224 | * |sin(alpha), cos(alpha), 0 | . |0 , 1, 0 | . |0, cos(gamma), -sin(gamma)| . |y| = |y1|
|
|---|
| 225 | * [0 , 0 , 1 ] [-sin(beta), 0, cos(beta)] [0, sin(gamma), cos(gamma) ] [z] [z1]
|
|---|
| 226 | * which becomes
|
|---|
| 227 | * x1 = y(cos(alpha)sin(beta)sin(gamma) - sin(alpha)cos(gamma)) + z(cos(alpha)sin(beta)cos(gamma) + sin(alpha)sin(gamma))
|
|---|
| 228 | * + x cos(alpha)cos(beta)
|
|---|
| 229 | * y1 = y(sin(alpha)sin(beta)sin(gamma) + cos(alpha)cos(gamma)) + z(sin(alpha)sin(beta)cos(gamma) - cos(alpha)sin(gamma))
|
|---|
| 230 | * + x sin(alpha)cos(beta)
|
|---|
| 231 | * z1 = y cos(beta)sin(gamma) + z cos(beta)cos(gamma) - x sin(beta)
|
|---|
| 232 | */
|
|---|
| 233 | // @formatting:on
|
|---|
| 234 | double vecX;
|
|---|
| 235 | double vecY;
|
|---|
| 236 | double vecZ;
|
|---|
| 237 | // We only do pitch/roll (we specifically do not do roll -- this would lead to tilting the image)
|
|---|
| 238 | // So yaw (alpha) -> azimuthalAngle, pitch (beta) -> polarAngle, roll (gamma) -> 0 (sin(gamma) -> 0, cos(gamma) -> 1)
|
|---|
| 239 | // gamma is set here just to make it slightly easier to tilt images in the future -- we just have to set the gamma somewhere else.
|
|---|
| 240 | // Ironically enough, the alpha (yaw) and gama (roll) got reversed somewhere. TODO figure out where and fix this.
|
|---|
| 241 | final int gamma = 0;
|
|---|
| 242 | final double sinAlpha = Math.sin(gamma);
|
|---|
| 243 | final double cosAlpha = Math.cos(gamma);
|
|---|
| 244 | final double cosGamma = this.rotation.getAzimuthalAngleCos();
|
|---|
| 245 | final double sinGamma = this.rotation.getAzimuthalAngleSin();
|
|---|
| 246 | final double cosBeta = this.rotation.getPolarAngleCos();
|
|---|
| 247 | final double sinBeta = this.rotation.getPolarAngleSin();
|
|---|
| 248 | final double x = vec.getX();
|
|---|
| 249 | final double y = YAW_DIRECTION * vec.getY();
|
|---|
| 250 | final double z = vec.getZ();
|
|---|
| 251 | vecX = y * (cosAlpha * sinBeta * sinGamma - sinAlpha * cosGamma)
|
|---|
| 252 | + z * (cosAlpha * sinBeta * cosGamma + sinAlpha * sinGamma) + x * cosAlpha * cosBeta;
|
|---|
| 253 | vecY = y * (sinAlpha * sinBeta * sinGamma + cosAlpha * cosGamma)
|
|---|
| 254 | + z * (sinAlpha * sinBeta * cosGamma - cosAlpha * sinGamma) + x * sinAlpha * cosBeta;
|
|---|
| 255 | vecZ = y * cosBeta * sinGamma + z * cosBeta * cosGamma - x * sinBeta;
|
|---|
| 256 | return new Vector3D(vecX, YAW_DIRECTION * vecY, vecZ);
|
|---|
| 257 | }
|
|---|
| 258 |
|
|---|
| 259 | /** Maps a panoramic view of sourceImage into targetImage based on current configuration of Camera Plane
|
|---|
| 260 | * @param sourceImage The image to paint
|
|---|
| 261 | * @param targetImage The target image
|
|---|
| 262 | * @param visibleRect The part of target image which will be visible
|
|---|
| 263 | */
|
|---|
| 264 | public void mapping(BufferedImage sourceImage, BufferedImage targetImage, Rectangle visibleRect) {
|
|---|
| 265 | DataBuffer sourceBuffer = sourceImage.getRaster().getDataBuffer();
|
|---|
| 266 | DataBuffer targetBuffer = targetImage.getRaster().getDataBuffer();
|
|---|
| 267 | // Faster mapping
|
|---|
| 268 | if (sourceBuffer.getDataType() == DataBuffer.TYPE_BYTE && targetBuffer.getDataType() == DataBuffer.TYPE_BYTE) {
|
|---|
| 269 | commonFastByteMapping(sourceImage, targetImage, visibleRect);
|
|---|
| 270 | } else if (sourceBuffer.getDataType() == DataBuffer.TYPE_INT
|
|---|
| 271 | && targetBuffer.getDataType() == DataBuffer.TYPE_INT) {
|
|---|
| 272 | int[] sourceImageBuffer = ((DataBufferInt) sourceImage.getRaster().getDataBuffer()).getData();
|
|---|
| 273 | int[] targetImageBuffer = ((DataBufferInt) targetImage.getRaster().getDataBuffer()).getData();
|
|---|
| 274 | IntStream.range(visibleRect.y, visibleRect.y + visibleRect.height).parallel()
|
|---|
| 275 | .forEach(y -> IntStream.range(visibleRect.x, visibleRect.x + visibleRect.width).forEach(x -> {
|
|---|
| 276 | final Point2D.Double p = mapPoint(x, y);
|
|---|
| 277 | int tx = (int) (p.x * (sourceImage.getWidth() - 1));
|
|---|
| 278 | int ty = (int) (p.y * (sourceImage.getHeight() - 1));
|
|---|
| 279 | int color = sourceImageBuffer[ty * sourceImage.getWidth() + tx];
|
|---|
| 280 | targetImageBuffer[y * targetImage.getWidth() + x] = color;
|
|---|
| 281 | }));
|
|---|
| 282 | } else if (sourceBuffer.getDataType() == DataBuffer.TYPE_DOUBLE && targetBuffer.getDataType() == DataBuffer.TYPE_DOUBLE) {
|
|---|
| 283 | double[] sourceImageBuffer = ((DataBufferDouble) sourceImage.getRaster().getDataBuffer()).getData();
|
|---|
| 284 | double[] targetImageBuffer = ((DataBufferDouble) targetImage.getRaster().getDataBuffer()).getData();
|
|---|
| 285 | IntStream.range(visibleRect.y, visibleRect.y + visibleRect.height).parallel()
|
|---|
| 286 | .forEach(y -> IntStream.range(visibleRect.x, visibleRect.x + visibleRect.width).forEach(x -> {
|
|---|
| 287 | final Point2D.Double p = mapPoint(x, y);
|
|---|
| 288 | int tx = (int) (p.x * (sourceImage.getWidth() - 1));
|
|---|
| 289 | int ty = (int) (p.y * (sourceImage.getHeight() - 1));
|
|---|
| 290 | double color = sourceImageBuffer[ty * sourceImage.getWidth() + tx];
|
|---|
| 291 | targetImageBuffer[y * targetImage.getWidth() + x] = color;
|
|---|
| 292 | }));
|
|---|
| 293 | } else {
|
|---|
| 294 | IntStream.range(visibleRect.y, visibleRect.y + visibleRect.height).parallel()
|
|---|
| 295 | .forEach(y -> IntStream.range(visibleRect.x, visibleRect.x + visibleRect.width).parallel().forEach(x -> {
|
|---|
| 296 | final Point2D.Double p = mapPoint(x, y);
|
|---|
| 297 | targetImage.setRGB(x, y, sourceImage.getRGB((int) (p.x * (sourceImage.getWidth() - 1)),
|
|---|
| 298 | (int) (p.y * (sourceImage.getHeight() - 1))));
|
|---|
| 299 | }));
|
|---|
| 300 | }
|
|---|
| 301 | }
|
|---|
| 302 |
|
|---|
| 303 | private void commonFastByteMapping(BufferedImage sourceImage, BufferedImage targetImage, Rectangle visibleRect) {
|
|---|
| 304 | final byte[] sourceImageBuffer = ((DataBufferByte) sourceImage.getRaster().getDataBuffer()).getData();
|
|---|
| 305 | final byte[] targetImageBuffer = ((DataBufferByte) targetImage.getRaster().getDataBuffer()).getData();
|
|---|
| 306 | final boolean sourceHasAlphaChannel = sourceImage.getAlphaRaster() != null;
|
|---|
| 307 | final boolean targetHasAlphaChannel = targetImage.getAlphaRaster() != null;
|
|---|
| 308 | final int sourcePixelLength = sourceHasAlphaChannel ? 4 : 3;
|
|---|
| 309 | final int targetPixelLength = targetHasAlphaChannel ? 4 : 3;
|
|---|
| 310 | final int addSourceAlpha = sourceHasAlphaChannel ? 1 : 0;
|
|---|
| 311 | final int addTargetAlpha = targetHasAlphaChannel ? 1 : 0;
|
|---|
| 312 | IntStream.range(visibleRect.y, visibleRect.y + visibleRect.height).parallel()
|
|---|
| 313 | .forEach(y -> IntStream.range(visibleRect.x, visibleRect.x + visibleRect.width).forEach(x -> {
|
|---|
| 314 | final Point2D.Double p = mapPoint(x, y);
|
|---|
| 315 | int tx = ((int) (p.x * (sourceImage.getWidth() - 1)));
|
|---|
| 316 | int ty = ((int) (p.y * (sourceImage.getHeight() - 1)));
|
|---|
| 317 | int sourceOffset = (ty * sourceImage.getWidth() + tx) * sourcePixelLength;
|
|---|
| 318 | int targetOffset = (y * targetImage.getWidth() + x) * targetPixelLength;
|
|---|
| 319 | try {
|
|---|
| 320 | // Alpha, if present
|
|---|
| 321 | if (targetHasAlphaChannel) {
|
|---|
| 322 | byte a = sourceHasAlphaChannel ? sourceImageBuffer[sourceOffset] : (byte) 255;
|
|---|
| 323 | targetImageBuffer[targetOffset] = a;
|
|---|
| 324 | }
|
|---|
| 325 | // Blue
|
|---|
| 326 | targetImageBuffer[targetOffset + addTargetAlpha] = sourceImageBuffer[sourceOffset + addSourceAlpha];
|
|---|
| 327 | // Green
|
|---|
| 328 | targetImageBuffer[targetOffset + addTargetAlpha + 1] = sourceImageBuffer[sourceOffset + addSourceAlpha + 1];
|
|---|
| 329 | // Red
|
|---|
| 330 | targetImageBuffer[targetOffset + addTargetAlpha + 2] = sourceImageBuffer[sourceOffset + addSourceAlpha + 2];
|
|---|
| 331 | } catch (ArrayIndexOutOfBoundsException aioobe) {
|
|---|
| 332 | // For debugging #22590, #23055, and #23697
|
|---|
| 333 | throw BugReport.intercept(aioobe)
|
|---|
| 334 | .put("visibleRect", visibleRect)
|
|---|
| 335 | .put("sourceImageBuffer", sourceImageBuffer.length)
|
|---|
| 336 | .put("targetImageBuffer", targetImageBuffer.length)
|
|---|
| 337 | .put("sourceHasAlphaChannel", sourceHasAlphaChannel)
|
|---|
| 338 | .put("targetHasAlphaChannel", targetHasAlphaChannel);
|
|---|
| 339 | }
|
|---|
| 340 | }));
|
|---|
| 341 | }
|
|---|
| 342 |
|
|---|
| 343 | /**
|
|---|
| 344 | * Map a real point to the displayed point. This method uses cached vectors.
|
|---|
| 345 | * @param x The original x coordinate
|
|---|
| 346 | * @param y The original y coordinate
|
|---|
| 347 | * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}.
|
|---|
| 348 | */
|
|---|
| 349 | public final Point2D.Double mapPoint(final int x, final int y) {
|
|---|
| 350 | final Vector3D vec = getVector3D(x, y);
|
|---|
| 351 | return UVMapping.getTextureCoordinate(vec);
|
|---|
| 352 | }
|
|---|
| 353 |
|
|---|
| 354 | /**
|
|---|
| 355 | * Map a real point to the displayed point. This function does not use cached vectors.
|
|---|
| 356 | * @param x The original x coordinate
|
|---|
| 357 | * @param y The original y coordinate
|
|---|
| 358 | * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}.
|
|---|
| 359 | */
|
|---|
| 360 | public final Point2D.Double mapPoint(final double x, final double y) {
|
|---|
| 361 | final Vector3D vec = getVector3D(x, y);
|
|---|
| 362 | return UVMapping.getTextureCoordinate(vec);
|
|---|
| 363 | }
|
|---|
| 364 | }
|
|---|