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}