source: josm/trunk/src/org/openstreetmap/josm/gui/util/imagery/CameraPlane.java

Last change on this file was 19088, checked in by taylor.smock, 19 months ago

See #22590, #23055, and #23697: Add additional information to bug report to (hopefully) figure out what is going on

This additionally reduces duplicated code, where the duplicated code differed
only in a few variables.

  • Property svn:eol-style set to native
File size: 16.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.util.imagery;
3
4import java.awt.Point;
5import java.awt.Rectangle;
6import java.awt.geom.Point2D;
7import java.awt.image.BufferedImage;
8import java.awt.image.DataBuffer;
9import java.awt.image.DataBufferByte;
10import java.awt.image.DataBufferDouble;
11import java.awt.image.DataBufferInt;
12import java.util.stream.IntStream;
13
14import org.openstreetmap.josm.tools.Logging;
15
16import jakarta.annotation.Nullable;
17import org.openstreetmap.josm.tools.bugreport.BugReport;
18
19/**
20 * The plane that the camera appears on and rotates around.
21 * @since 18246
22 */
23public 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}
Note: See TracBrowser for help on using the repository browser.