001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.util.imagery; 003 004import java.awt.Point; 005import java.awt.geom.Point2D; 006import java.awt.image.BufferedImage; 007import java.awt.image.DataBuffer; 008import java.awt.image.DataBufferDouble; 009import java.awt.image.DataBufferInt; 010import java.util.stream.IntStream; 011import javax.annotation.Nullable; 012 013/** 014 * The plane that the camera appears on and rotates around. 015 * @since 18246 016 */ 017public class CameraPlane { 018 /** The field of view for the panorama at 0 zoom */ 019 static final double PANORAMA_FOV = Math.toRadians(110); 020 021 /** This determines the yaw direction. We may want to make it a config option, but maybe not */ 022 private static final byte YAW_DIRECTION = -1; 023 024 /** The width of the image */ 025 private final int width; 026 /** The height of the image */ 027 private final int height; 028 029 private final Vector3D[][] vectors; 030 private Vector3D rotation; 031 032 public static final double HALF_PI = Math.PI / 2; 033 public static final double TWO_PI = 2 * Math.PI; 034 035 /** 036 * Create a new CameraPlane with the default FOV (110 degrees). 037 * 038 * @param width The width of the image 039 * @param height The height of the image 040 */ 041 public CameraPlane(int width, int height) { 042 this(width, height, (width / 2d) / Math.tan(PANORAMA_FOV / 2)); 043 } 044 045 /** 046 * Create a new CameraPlane 047 * 048 * @param width The width of the image 049 * @param height The height of the image 050 * @param distance The radial distance of the photosphere 051 */ 052 private CameraPlane(int width, int height, double distance) { 053 this.width = width; 054 this.height = height; 055 this.rotation = new Vector3D(Vector3D.VectorType.RPA, distance, 0, 0); 056 this.vectors = new Vector3D[width][height]; 057 IntStream.range(0, this.height).parallel().forEach(y -> IntStream.range(0, this.width).parallel() 058 .forEach(x -> this.vectors[x][y] = this.getVector3D((double) x, y))); 059 } 060 061 /** 062 * Get the width of the image 063 * @return The width of the image 064 */ 065 public int getWidth() { 066 return this.width; 067 } 068 069 /** 070 * Get the height of the image 071 * @return The height of the image 072 */ 073 public int getHeight() { 074 return this.height; 075 } 076 077 /** 078 * Get the point for a vector 079 * 080 * @param vector the vector for which the corresponding point on the camera plane will be returned 081 * @return the point on the camera plane to which the given vector is mapped, nullable 082 */ 083 @Nullable 084 public Point getPoint(final Vector3D vector) { 085 final Vector3D rotatedVector = rotate(vector); 086 // Currently set to false due to change in painting 087 if (rotatedVector.getZ() < 0) { 088 // Ignores any points "behind the back", so they don't get painted a second time on the other 089 // side of the sphere 090 return null; 091 } 092 // This is a slightly faster than just doing the (brute force) method of Math.max(Math.min)). Reduces if 093 // statements by 1 per call. 094 final long x = Math 095 .round((rotatedVector.getX() / rotatedVector.getZ()) * this.rotation.getRadialDistance() + width / 2d); 096 final long y = Math 097 .round((rotatedVector.getY() / rotatedVector.getZ()) * this.rotation.getRadialDistance() + height / 2d); 098 099 try { 100 return new Point(Math.toIntExact(x), Math.toIntExact(y)); 101 } catch (ArithmeticException e) { 102 return new Point((int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, x)), 103 (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, y))); 104 } 105 } 106 107 /** 108 * Convert a point to a 3D vector 109 * 110 * @param p The point to convert 111 * @return The vector 112 */ 113 public Vector3D getVector3D(final Point p) { 114 return this.getVector3D(p.x, p.y); 115 } 116 117 /** 118 * Convert a point to a 3D vector (vectors are cached) 119 * 120 * @param x The x coordinate 121 * @param y The y coordinate 122 * @return The vector 123 */ 124 public Vector3D getVector3D(final int x, final int y) { 125 Vector3D res; 126 try { 127 res = rotate(vectors[x][y]); 128 } catch (Exception e) { 129 res = Vector3D.DEFAULT_VECTOR_3D; 130 } 131 return res; 132 } 133 134 /** 135 * Convert a point to a 3D vector. Warning: This method does not cache. 136 * 137 * @param x The x coordinate 138 * @param y The y coordinate 139 * @return The vector (the middle of the image is 0, 0) 140 */ 141 public Vector3D getVector3D(final double x, final double y) { 142 return new Vector3D(x - width / 2d, y - height / 2d, this.rotation.getRadialDistance()).normalize(); 143 } 144 145 /** 146 * Set camera plane rotation by current plane position. 147 * 148 * @param p Point within current plane. 149 */ 150 public void setRotation(final Point p) { 151 setRotation(getVector3D(p)); 152 } 153 154 /** 155 * Set the rotation from the difference of two points 156 * 157 * @param from The originating point 158 * @param to The new point 159 */ 160 public void setRotationFromDelta(final Point from, final Point to) { 161 // Bound check (bounds are essentially the image viewer component) 162 if (from.x < 0 || from.y < 0 || to.x < 0 || to.y < 0 163 || from.x > this.vectors.length - 1 || from.y > this.vectors[from.x].length - 1 164 || to.x > this.vectors.length - 1 || to.y > this.vectors[to.x].length - 1) { 165 return; 166 } 167 Vector3D f1 = this.vectors[from.x][from.y]; 168 Vector3D t1 = this.vectors[to.x][to.y]; 169 double deltaPolarAngle = f1.getPolarAngle() - t1.getPolarAngle(); 170 double deltaAzimuthalAngle = t1.getAzimuthalAngle() - f1.getAzimuthalAngle(); 171 double polarAngle = this.rotation.getPolarAngle() + deltaPolarAngle; 172 double azimuthalAngle = this.rotation.getAzimuthalAngle() + deltaAzimuthalAngle; 173 this.setRotation(azimuthalAngle, polarAngle); 174 } 175 176 /** 177 * Set camera plane rotation by spherical vector. 178 * 179 * @param vec vector pointing new view position. 180 */ 181 public void setRotation(Vector3D vec) { 182 setRotation(vec.getPolarAngle(), vec.getAzimuthalAngle()); 183 } 184 185 public synchronized Vector3D getRotation() { 186 return this.rotation; 187 } 188 189 synchronized void setRotation(double azimuthalAngle, double polarAngle) { 190 // Note: Something, somewhere, is switching the two. 191 // FIXME: Figure out what is switching them and why 192 // Prevent us from going much outside 2pi 193 if (polarAngle < 0) { 194 polarAngle = polarAngle + TWO_PI; 195 } else if (polarAngle > TWO_PI) { 196 polarAngle = polarAngle - TWO_PI; 197 } 198 // Avoid flipping the camera 199 if (azimuthalAngle > HALF_PI) { 200 azimuthalAngle = HALF_PI; 201 } else if (azimuthalAngle < -HALF_PI) { 202 azimuthalAngle = -HALF_PI; 203 } 204 this.rotation = new Vector3D(Vector3D.VectorType.RPA, this.rotation.getRadialDistance(), polarAngle, azimuthalAngle); 205 } 206 207 /** 208 * Rotate a vector using the current rotation 209 * @param vec The vector to rotate 210 * @return A rotated vector 211 */ 212 private Vector3D rotate(final Vector3D vec) { 213 // @formatting:off 214 /* Full rotation matrix for a yaw-pitch-roll 215 * yaw = alpha, pitch = beta, roll = gamma (typical representations) 216 * [cos(alpha), -sin(alpha), 0 ] [cos(beta), 0, sin(beta) ] [1, 0 , 0 ] [x] [x1] 217 * |sin(alpha), cos(alpha), 0 | . |0 , 1, 0 | . |0, cos(gamma), -sin(gamma)| . |y| = |y1| 218 * [0 , 0 , 1 ] [-sin(beta), 0, cos(beta)] [0, sin(gamma), cos(gamma) ] [z] [z1] 219 * which becomes 220 * x1 = y(cos(alpha)sin(beta)sin(gamma) - sin(alpha)cos(gamma)) + z(cos(alpha)sin(beta)cos(gamma) + sin(alpha)sin(gamma)) 221 * + x cos(alpha)cos(beta) 222 * y1 = y(sin(alpha)sin(beta)sin(gamma) + cos(alpha)cos(gamma)) + z(sin(alpha)sin(beta)cos(gamma) - cos(alpha)sin(gamma)) 223 * + x sin(alpha)cos(beta) 224 * z1 = y cos(beta)sin(gamma) + z cos(beta)cos(gamma) - x sin(beta) 225 */ 226 // @formatting:on 227 double vecX; 228 double vecY; 229 double vecZ; 230 // We only do pitch/roll (we specifically do not do roll -- this would lead to tilting the image) 231 // So yaw (alpha) -> azimuthalAngle, pitch (beta) -> polarAngle, roll (gamma) -> 0 (sin(gamma) -> 0, cos(gamma) -> 1) 232 // 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. 233 // Ironically enough, the alpha (yaw) and gama (roll) got reversed somewhere. TODO figure out where and fix this. 234 final int gamma = 0; 235 final double sinAlpha = Math.sin(gamma); 236 final double cosAlpha = Math.cos(gamma); 237 final double cosGamma = this.rotation.getAzimuthalAngleCos(); 238 final double sinGamma = this.rotation.getAzimuthalAngleSin(); 239 final double cosBeta = this.rotation.getPolarAngleCos(); 240 final double sinBeta = this.rotation.getPolarAngleSin(); 241 final double x = vec.getX(); 242 final double y = YAW_DIRECTION * vec.getY(); 243 final double z = vec.getZ(); 244 vecX = y * (cosAlpha * sinBeta * sinGamma - sinAlpha * cosGamma) 245 + z * (cosAlpha * sinBeta * cosGamma + sinAlpha * sinGamma) + x * cosAlpha * cosBeta; 246 vecY = y * (sinAlpha * sinBeta * sinGamma + cosAlpha * cosGamma) 247 + z * (sinAlpha * sinBeta * cosGamma - cosAlpha * sinGamma) + x * sinAlpha * cosBeta; 248 vecZ = y * cosBeta * sinGamma + z * cosBeta * cosGamma - x * sinBeta; 249 return new Vector3D(vecX, YAW_DIRECTION * vecY, vecZ); 250 } 251 252 public void mapping(BufferedImage sourceImage, BufferedImage targetImage) { 253 DataBuffer sourceBuffer = sourceImage.getRaster().getDataBuffer(); 254 DataBuffer targetBuffer = targetImage.getRaster().getDataBuffer(); 255 // Faster mapping 256 if (sourceBuffer.getDataType() == DataBuffer.TYPE_INT && targetBuffer.getDataType() == DataBuffer.TYPE_INT) { 257 int[] sourceImageBuffer = ((DataBufferInt) sourceImage.getRaster().getDataBuffer()).getData(); 258 int[] targetImageBuffer = ((DataBufferInt) targetImage.getRaster().getDataBuffer()).getData(); 259 IntStream.range(0, targetImage.getHeight()).parallel() 260 .forEach(y -> IntStream.range(0, targetImage.getWidth()).forEach(x -> { 261 final Point2D.Double p = mapPoint(x, y); 262 int tx = (int) (p.x * (sourceImage.getWidth() - 1)); 263 int ty = (int) (p.y * (sourceImage.getHeight() - 1)); 264 int color = sourceImageBuffer[ty * sourceImage.getWidth() + tx]; 265 targetImageBuffer[y * targetImage.getWidth() + x] = color; 266 })); 267 } else if (sourceBuffer.getDataType() == DataBuffer.TYPE_DOUBLE && targetBuffer.getDataType() == DataBuffer.TYPE_DOUBLE) { 268 double[] sourceImageBuffer = ((DataBufferDouble) sourceImage.getRaster().getDataBuffer()).getData(); 269 double[] targetImageBuffer = ((DataBufferDouble) targetImage.getRaster().getDataBuffer()).getData(); 270 IntStream.range(0, targetImage.getHeight()).parallel() 271 .forEach(y -> IntStream.range(0, targetImage.getWidth()).forEach(x -> { 272 final Point2D.Double p = mapPoint(x, y); 273 int tx = (int) (p.x * (sourceImage.getWidth() - 1)); 274 int ty = (int) (p.y * (sourceImage.getHeight() - 1)); 275 double color = sourceImageBuffer[ty * sourceImage.getWidth() + tx]; 276 targetImageBuffer[y * targetImage.getWidth() + x] = color; 277 })); 278 } else { 279 IntStream.range(0, targetImage.getHeight()).parallel() 280 .forEach(y -> IntStream.range(0, targetImage.getWidth()).parallel().forEach(x -> { 281 final Point2D.Double p = mapPoint(x, y); 282 targetImage.setRGB(x, y, sourceImage.getRGB((int) (p.x * (sourceImage.getWidth() - 1)), 283 (int) (p.y * (sourceImage.getHeight() - 1)))); 284 })); 285 } 286 } 287 288 /** 289 * Map a real point to the displayed point. This method uses cached vectors. 290 * @param x The original x coordinate 291 * @param y The original y coordinate 292 * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}. 293 */ 294 public final Point2D.Double mapPoint(final int x, final int y) { 295 final Vector3D vec = getVector3D(x, y); 296 return UVMapping.getTextureCoordinate(vec); 297 } 298 299 /** 300 * Map a real point to the displayed point. This function does not use cached vectors. 301 * @param x The original x coordinate 302 * @param y The original y coordinate 303 * @return The scaled (0-1) point in the image. Use {@code p.x * (image.getWidth() - 1)} or {@code p.y * image.getHeight() - 1}. 304 */ 305 public final Point2D.Double mapPoint(final double x, final double y) { 306 final Vector3D vec = getVector3D(x, y); 307 return UVMapping.getTextureCoordinate(vec); 308 } 309}