package ch.ethz.karto.map3d;
import ch.ethz.karto.map3d.gui.Map3DOptionsPanel;
import javax.media.opengl.DebugGL2;
import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;
import ika.gui.GUIUtil;
import ika.utils.ErrorDialog;
import ika.utils.TextWindow;
import java.awt.Color;
import java.awt.Component;
import java.awt.Frame;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.media.opengl.awt.GLJPanel;
import com.jogamp.opengl.util.awt.Screenshot;
/**
* This class implements a Map3D viewer.
*/
public class Map3DViewer extends GLCanvas implements GLEventListener {
private static final float MAX_CYLINDER_FOV = 165f;
private boolean highResCylindricalRendering = false;
public void setHighResCylindricalRendering(boolean useHighRes) {
if (useHighRes != highResCylindricalRendering) {
highResCylindricalRendering = useHighRes;
if (this.camera == Camera.cylindrical) {
this.updateView();
}
}
}
private boolean bindLightDirectionToViewDirection = false;
public void setBindLightDirectionToViewDirection(boolean bind) {
if (this.bindLightDirectionToViewDirection != bind) {
this.bindLightDirectionToViewDirection = bind;
if (this.camera == Camera.cylindrical) {
this.updateView();
}
}
}
public enum Camera {
perspective, parallelOblique, planOblique, orthogonal, cylindrical
}
public static String camera(Camera camera) {
switch (camera) {
case perspective:
return "Perspective View";
case parallelOblique:
return "Parallel Oblique";
case planOblique:
return "Plan Oblique";
case orthogonal:
return "2D Orthogonal";
case cylindrical:
return "Cylindrical";
default:
return "";
}
}
public static Camera camera(String name) {
if ("Perspective View".equals(name)) {
return Camera.perspective;
}
if ("Parallel Oblique".equals(name)) {
return Camera.parallelOblique;
}
if ("Plan Oblique".equals(name)) {
return Camera.planOblique;
}
if ("2D Orthogonal".equals(name)) {
return Camera.orthogonal;
}
if ("Cylindrical".equals(name)) {
return Camera.cylindrical;
}
return Camera.perspective;
}
/**
* only use glu in callbacks
*/
private GLU glu_callback_only;
private Component component;
private GLAutoDrawable drawable;
private Map3DModel model;
private Map3DTexture texture;
private Map3DAnimation animation;
private boolean antialiasing = true;
private Map3DMouseHandler mouseHandler;
/**
* Default rotation around x-axis. Positive values between 0 and 90 degrees.
* 0 degree corresponds to vertical view, 90 degrees to a horizontal view.
*/
private float defaultXAngle = 55.0f;
/**
* Default rotation around z-axis. Positive values corresponds to a
* clockwise rotation. Units in degrees.
*/
private float defaultZAngle = 0.0f;
/**
* Default view angle. A value of 25 degrees corresponds roughly to an
* object of 25 cm size held at an eye distance of 50 cm.
*/
private float defaultViewAngle = 25.0f;
private float defaultShiftX = 0f;
private float defaultShiftY = 0f;
/**
* Minimum and maximum angle around x-axis.
*/
protected static final float MIN_X_ANGLE = 1.0f, MAX_X_ANGLE = 90.0f;
/**
* Minimum, maximum, and default distance from model.
*/
//private static final float MIN_DISTANCE = 1.2f, MAX_DISTANCE = 3.6f, DEFAULT_DISTANCE = 2.4f;
public static final float MIN_DISTANCE = 0.2f, MAX_DISTANCE = 3f;
private float defaultDistance = 2.4f;
private float defaultCylindricalHeight = 0.2f;
/**
* Rotation around the x axis in degrees.
*/
protected float xAngle = defaultXAngle;
/**
* Rotation around the z axis in degrees.
*/
protected float zAngle = defaultZAngle;
/**
* Distance of camera from origin
*/
protected float viewDistance = defaultDistance;
/**
* Height of cylindrical camera
*/
private float cylindricalHeight = defaultCylindricalHeight;
/**
* Field of view of camera
*/
protected float fov = defaultViewAngle;
protected float shearX = 0;
protected float shearY = 0;
protected float shiftX = defaultShiftX;
protected float shiftY = defaultShiftY;
private float bgRed = 1.0f;
private float bgGreen = 1.0f;
private float bgBlue = 1.0f;
protected boolean shadingEnabled = true;
private Camera camera = Camera.planOblique;//Camera.perspective;
protected static final float MIN_SHEAR_ANGLE = 0.0f, MAX_SHEAR_ANGLE = 180.0f;
private static final float DEFAULT_LIGHT_AMBIENT = 0.4f;
private static final float DEFAULT_LIGHT_DIFFUSE = 0.6f;
private static final float DEFAULT_LIGHT_AZIMUTH = 315; //225;
private static final float DEFAULT_LIGHT_ZENITH = 45;
private float ambientLight = DEFAULT_LIGHT_AMBIENT;
private float diffuseLight = DEFAULT_LIGHT_DIFFUSE;
private float lightAzimuth = DEFAULT_LIGHT_AZIMUTH;
private float lightZenith = DEFAULT_LIGHT_ZENITH;
private static final float Z_NEAR = 0.001f;
private static final float Z_FAR = 100.0f;
private float aspectRatio_callback_only = -1;
private static final int DEF_CYLINDER_IMAGES_COUNT = 8;
private boolean fogEnabled = false;
private float fogStart = 0f;
private float fogEnd = 1f;
private Color fogColor = Color.WHITE;
/**
* @return the antialiasing
*/
public boolean isAntialiasing() {
return antialiasing;
}
/**
* @param antialiasing the antialiasing to set
*/
public void setAntialiasing(boolean antialiasing) {
this.antialiasing = antialiasing;
}
private int getCylindricalImagesCount() {
if (this.highResCylindricalRendering) {
return this.drawable.getContext().getGLDrawable().getWidth();
} else {
return DEF_CYLINDER_IMAGES_COUNT;
}
}
public static enum GLComponentType {
GL_AWT, GL_Swing
};
/**
* Map3D viewer constructor
*
* @param glComponentType With GL_AWT a heavyweight AWT component GLCanvas
* is created. With GL_Swing a lightweight GLJPanel is created. GLCanvas
* offers faster rendering, but may cause problems when combined with other
* Swing components with certain layout managers.
*/
public Map3DViewer(GLComponentType glComponentType, GLProfile glProfile) {
this(glComponentType, glProfile, null);
}
/**
* Map3D viewer constructor
*
* @param glComponentType With GL_AWT a heavyweight AWT component GLCanvas
* is created. With GL_Swing a lightweight GLJPanel is created. GLCanvas
* offers faster rendering, but may cause problems when combined with other
* Swing components with certain layout managers.
*/
public Map3DViewer(GLComponentType glComponentType, GLProfile glProfile, Map3DModel model) {
this.init(glComponentType, glProfile, model);
}
private void init(GLComponentType glComponentType, GLProfile profile, Map3DModel model) {
GLCapabilities caps = new GLCapabilities(profile);
// use sample buffers for antialiasing
caps.setSampleBuffers(true);
// set the number of supersampling for antialising
caps.setNumSamples(Map3DOptionsPanel.getAntialiasingLevel());
if (glComponentType == GLComponentType.GL_Swing) {
this.component = new GLJPanel(caps);
} else {
this.component = new GLCanvas(caps);
}
this.drawable = (GLAutoDrawable)this.component;
this.component.setSize(1024, 768);
//((Component) this.component).setIgnoreRepaint(true);
this.drawable.addGLEventListener(this);
if (model == null) {
model = new Map3DModelVBOShader();
if (!model.canRun()) {
model = new Map3DModelVBO();
}
if (!model.canRun()) {
model = new Map3DModelVertexArrays();
}
}
//model = new Map3DModelVertexArrays();
this.model = model;
this.texture = new Map3DTexture();
this.setAnimation(new Map3DRotationAnimation(this.drawable));
mouseHandler = new Map3DMouseHandler(this);
this.component.addMouseMotionListener(mouseHandler);
this.component.addMouseListener(mouseHandler);
this.component.addMouseWheelListener(mouseHandler);
}
public BufferedImage getImage() {
// Needs the "current context"!
this.drawable.getContext().makeCurrent();
int width = component.getWidth();
int height = component.getHeight();
return Screenshot.readToBufferedImage(width, height);
}
public Map3DModel getModel(){
return this.model;
}
public void setModel(float grid[][], float cellSize) {
Map3DTexture1DMapper mapper = new Map3DNonLinearTexture1DMapper(grid);
this.setModel(grid, cellSize, mapper);
}
public void setModel(float[][] grid, float cellSize, Map3DTexture1DMapper mapper) {
this.model.setModel(grid, cellSize, mapper);
this.updateView();
}
public void setTextureImage(BufferedImage textureImage) {
this.texture.setTexture(textureImage);
this.updateView();
}
public void clearTextureImage() {
this.texture.clearTexture();
this.updateView();
}
public boolean hasTexture() {
return this.texture.hasTexture();
}
public boolean isTexture1D() {
return this.texture.is1D();
}
/**
* Enables or disables light.
*
* @param enable turns light on or off
*/
public void setShading(boolean enable) {
this.shadingEnabled = enable;
this.updateView();
}
public boolean isShading() {
return shadingEnabled;
}
public void enableFog(GL gl1) {
GL2 gl = (GL2)gl1;
if (this.fogEnabled) {
gl.glEnable(GL2.GL_FOG);
float r = fogColor.getRed() / 255f;
float g = fogColor.getGreen() / 255f;
float b = fogColor.getBlue() / 255f;
gl.glFogfv(GL2.GL_FOG_COLOR, new float[]{r, g, b}, 0);
float start = fogStart;
if (camera != Camera.cylindrical) {
start += viewDistance - 0.5f;
}
gl.glFogf(GL2.GL_FOG_START, start);
float end = fogEnd;
if (camera != Camera.cylindrical) {
end += viewDistance - 0.5f;
}
gl.glFogf(GL2.GL_FOG_END, end);
gl.glFogi(GL2.GL_FOG_MODE, GL2.GL_LINEAR);
} else {
gl.glDisable(GL2.GL_FOG);
}
}
/**
* Get the OpenGL component.
*
* @return The OpenGL component
*/
public Component getComponent() {
return (Component) this.component;
}
private void setupLight(GL gl1, double lightAzimuthDeg, double lightZenithDeg) {
GL2 gl = (GL2)gl1;
if (this.shadingEnabled) {
gl.glEnable(GL2.GL_LIGHTING);
gl.glEnable(GL2.GL_LIGHT0);
final double a = -Math.PI / 2 - Math.toRadians(lightAzimuthDeg);
final double z = Math.toRadians(lightZenithDeg);
final double sinz = Math.sin(z);
final float lx = (float) (Math.cos(a) * sinz);
final float ly = (float) (Math.sin(a) * sinz);
final float lz = (float) Math.cos(z);
float light_ambient[] = {ambientLight, ambientLight, ambientLight, 1.0f};
float light_diffuse[] = {diffuseLight, diffuseLight, diffuseLight, 1.0f};
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_AMBIENT, light_ambient, 0);
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_DIFFUSE, light_diffuse, 0);
gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_POSITION, new float[]{-lx, ly, lz, 0}, 0);
} else {
// FIXME
gl.glDisable(GL2.GL_LIGHTING);
gl.glDisable(GL2.GL_LIGHT0);
float lmodel_ambient[] = {0f, 0f, 0f, 1.0f};
gl.glLightModelfv(GL2.GL_LIGHT_MODEL_AMBIENT, lmodel_ambient, 0);
}
}
public void setLight(float ambient, float diffuse) {
this.ambientLight = ambient;
this.diffuseLight = diffuse;
this.getComponent().firePropertyChange("view", 0, 1);
}
public float getAmbientLight() {
return ambientLight;
}
public float getDiffuseLight() {
return diffuseLight;
}
public void setLightDirection(float azimuth, float zenith) {
this.lightAzimuth = azimuth;
this.lightZenith = zenith;
this.getComponent().firePropertyChange("view", 0, 1);
}
public float getLightAzimuth() {
return this.lightAzimuth;
}
public float getLightZenith() {
return this.lightZenith;
}
public void defaultShading() {
this.ambientLight = DEFAULT_LIGHT_AMBIENT;
this.diffuseLight = DEFAULT_LIGHT_DIFFUSE;
this.lightAzimuth = DEFAULT_LIGHT_AZIMUTH;
this.lightZenith = DEFAULT_LIGHT_ZENITH;
}
private boolean installDebugGL(GLAutoDrawable drawable) {
GL gl = drawable.getGL();
if (gl == null) {
return false;
}
drawable.setGL(new DebugGL2((GL2)gl));
System.out.println("OpenGL debug mode: glError() called automatically "
+ "after each API call.");
return true;
}
/**
* GLEventListener Initialize material property and light source.
*/
@Override
public void init(GLAutoDrawable drawable) {
// for debugging only
assert installDebugGL(drawable);
GL gl1 = drawable.getGL();
GL2 gl = (GL2)gl1;
glu_callback_only = new GLU();
gl.glShadeModel(GL2.GL_SMOOTH);
gl.glClearColor(this.bgRed, this.bgGreen, this.bgBlue, 1.0f);
gl.glEnable(GL2.GL_DEPTH_TEST); // depth test must be enabled
gl.glHint(GL2.GL_PERSPECTIVE_CORRECTION_HINT, GL2.GL_NICEST);
setupLight(gl, this.lightAzimuth, this.lightZenith);
if (this.shadingEnabled) {
gl.glEnable(GL2.GL_COLOR_MATERIAL);
gl.glColorMaterial(GL2.GL_FRONT, GL2.GL_AMBIENT);
gl.glColor3f(0.5f, 0.5f, 0.5f);
gl.glColorMaterial(GL2.GL_FRONT, GL2.GL_DIFFUSE);
gl.glColor3f(1.0f, 1.0f, 1.0f);
} else {
gl.glDisable(GL2.GL_COLOR_MATERIAL);
}
}
/**
* Computes the height of an image section for the cylindrical projection.
* The cylindrical projection is approximated by CYLINDER_IMAGES_COUNT image
* sections.
*
* @param sectionWidth The width of a section
* @return The height of a tile
*/
private int cylinderSectionHeight(int sectionWidth) {
// the horizontal section of the full circle that this tile displays
double a = Math.toRadians(360f / getCylindricalImagesCount());
// the vertical viewing angle
// avoid tangens of Pi/2
double viewAngleCorr = Math.min(fov, MAX_CYLINDER_FOV);
double b = Math.toRadians(viewAngleCorr);
// the height of the tile
double h = sectionWidth * Math.tan(b / 2) / Math.tan(a / 2);
return (int) h;
}
/**
* GLEventListener Called by the drawable to initiate OpenGL rendering by
* the client.
*
* @param drawable
*/
@Override
public void display(GLAutoDrawable drawable) {
try {
GL gl = drawable.getGL();
gl.glClearColor(this.bgRed, this.bgGreen, this.bgBlue, 1.0f);
gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT | GL2.GL_ACCUM_BUFFER_BIT);
if (camera == Camera.cylindrical) {
displayCylindricalProjection(drawable);
} else {
display_(drawable, this.zAngle);
}
testOpenGLError(gl);
} catch (Throwable e) {
assert displayExtendedErrorDialog(e);
// FIXME try catch block
model.releaseModel(drawable.getGL());
if (model instanceof Map3DModelVBOShader) {
Map3DModel model2 = new Map3DModelVBO();
model2.setModel(model.grid, model.cellSize, model.texture1DMapper);
model = model2;
}
String errorTitle = "Rendering Error";
Frame parent = GUIUtil.getOwnerFrame(getComponent());
ErrorDialog.showErrorDialog(e.getMessage(), errorTitle, e, parent);
}
}
private boolean displayExtendedErrorDialog(Throwable e) {
String errorTitle = "Rendering Error";
StringWriter sw = new StringWriter();
sw.write(errorTitle);
sw.write(System.getProperty("line.separator"));
sw.write("Model: ");
sw.write(model.getClass().getSimpleName());
sw.write(System.getProperty("line.separator"));
if (e.getMessage() != null) {
sw.write(e.getMessage());
sw.write(System.getProperty("line.separator"));
}
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.flush();
Frame parent = GUIUtil.getOwnerFrame(getComponent());
new TextWindow(parent, true, true, sw.toString(), errorTitle);
return true;
}
/**
* Throws an exception if an OpenGL error occured since the last call to
* <code>glGetError</code>.
*
* @param gl
*/
private void testOpenGLError(GL gl) {
int errCode = gl.glGetError();
if (errCode != GL2.GL_NO_ERROR) {
StringBuilder sb = new StringBuilder("An OpenGL error occured: Code ");
sb.append(errCode);
sb.append(" - ");
sb.append(glu_callback_only.gluErrorString(errCode));
throw new RuntimeException(sb.toString());
}
}
/**
* Renders multiple perspective images to simulate a cylindrical projection.
*
* @param drawable
*/
private void displayCylindricalProjection(GLAutoDrawable drawable) {
GL gl = drawable.getGL();
// store the initial viewport
int[] vp = new int[4];
gl.glGetIntegerv(GL2.GL_VIEWPORT, vp, 0);
try {
int compH = this.drawable.getContext().getGLDrawable().getHeight();
int compW = this.drawable.getContext().getGLDrawable().getWidth();
float zoom = (viewDistance - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE);
// width and height of a single image
int w = (int) (compW / getCylindricalImagesCount() / zoom);
int h = (int) (cylinderSectionHeight(w) / zoom);
// vertical position of the image stripe
int y = (int) ((compH - h) / 2);
// angle covered by each image
float rotAngle = 360f / getCylindricalImagesCount();
// render getCylindricalImagesCount() views, each rotated by rotAngle
for (int i = 0; i < getCylindricalImagesCount(); i++) {
// adjust the viewport: draw to a section of the available space
gl.glViewport(i * w, y, w, h);
// increas the rotation around the z axis
float zAngle_ = this.zAngle - i * rotAngle;
zAngle_ = Map3DViewer.normalizeZAngle(zAngle_);
// render image
display_(drawable, zAngle_);
if (this.highResCylindricalRendering) {
System.out.println("Column: " + i);
}
}
} finally {
// restore the initial viewport
gl.glViewport(vp[0], vp[1], vp[2], vp[3]);
}
}
private void display_(GLAutoDrawable drawable, float zAngle) {
if (this.model == null) {
return;
}
GL gl1 = drawable.getGL();
GL2 gl = (GL2)gl1;
// use antialiasing when mouse is not being dragged.
if (!this.mouseHandler.isDragging() && antialiasing) {
gl.glEnable(GL2.GL_MULTISAMPLE);
} else {
gl.glDisable(GL2.GL_MULTISAMPLE);
}
this.setupProjection(gl);
this.enableFog(gl);
// with standart orientation: up vector 0/1/0
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glLoadIdentity();
if (camera == Camera.cylindrical) {
glu_callback_only.gluLookAt(0, 0, 0, 0, 1, 0, 0, 0, -1);
} else {
// position the camera at 0/0/viewDistance,
// looking at 0/0/0
glu_callback_only.gluLookAt(0, 0, viewDistance, 0, 0, 0, 0, 1, 0);
}
// load the texture if necessary
boolean textureChanged = texture.constructTexture(gl);
if (textureChanged) {
model.textureChanged();
}
// hack for shearing with vertex shader
if (model instanceof Map3DModelVBOShader) {
if (camera != Camera.planOblique) {
((Map3DModelVBOShader) model).setShearing(0, 0);
} else {
((Map3DModelVBOShader) model).setShearing(shearX, shearY);
}
}
// construct the geometry model if necessary
model.loadModel(gl, this.texture);
float modelWidth = this.model.getNormalizedModelWidth();
float modelHeight = this.model.getNormalizedModelHeight();
final float dz;
switch (camera) {
case perspective:
case parallelOblique:
case cylindrical:
dz = -model.getCenterElevation();
break;
default:
dz = 0;
}
// transform light without shearing
{
gl.glPushMatrix();
// rotate
switch (camera) {
case perspective:
case parallelOblique:
gl.glRotatef(xAngle, 1.0f, 0.0f, 0.0f);
}
gl.glRotatef(zAngle, 0.0f, 0.0f, 1.0f);
gl.glTranslatef(-modelWidth / 2, -modelHeight / 2, dz);
float azimuth = this.lightAzimuth;
if (bindLightDirectionToViewDirection) {
azimuth -= zAngle;
}
setupLight(gl, azimuth, this.lightZenith);
gl.glPopMatrix();
}
gl.glPushMatrix();
// vertical and horizontal shift to place model in viewport
// FIXME
// hack for compensating missing vertical shift in Map3DModelVBOShader.gridTexture()
// gl.glTranslatef(0, 0, this.model.getNormalizedMinimumValue());
if (camera != Camera.cylindrical) {
gl.glTranslatef(shiftX, shiftY, 0);
}
/*
// shear for plan oblique rendering
if (camera == Camera.planOblique && (shearX != 0 || shearY != 0)) {
gl.glEnable(GL2.GL_NORMALIZE);
shearMatrix(gl, shearX, -shearY);
gl.glTranslatef(0, 0, -Map3DModel.ZOFFSET);
}
*/
// rotate
switch (camera) {
case perspective:
case parallelOblique:
gl.glRotatef(xAngle, 1, 0, 0);
break;
}
gl.glRotatef(zAngle, 0.0f, 0.0f, 1.0f);
// center position of cylindrical camera on origin
if (camera == Camera.cylindrical) {
gl.glTranslatef(-shiftX / 2f, -shiftY / 2f, -cylindricalHeight);
}
// center model on origin by shifting by half width and size of model
gl.glTranslatef(-modelWidth / 2, -modelHeight / 2, dz);
if (camera == Camera.planOblique) {
gl.glEnable(GL2.GL_NORMALIZE);
// translate for XY shearing, which is proportional to Z
gl.glTranslatef(0, 0, -Map3DModel.ZOFFSET);
}
model.draw(gl, shadingEnabled, fogEnabled);
gl.glPopMatrix();
gl.glFlush();
animation.update(this);
}
private void setupProjection(GL gl1) {
GL2 gl = (GL2)gl1;
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glLoadIdentity();
switch (camera) {
case perspective:
double top = Math.tan(Math.toRadians(fov * 0.5)) * Z_NEAR;
double bottom = -top;
double left = aspectRatio_callback_only * bottom;
double right = aspectRatio_callback_only * top;
gl.glFrustum(left, right, bottom, top, Z_NEAR, Z_FAR);
//glu_callback_only.gluPerspective(fov, aspectRatio_callback_only, Z_NEAR, Z_FAR);
break;
case cylindrical: {
int compW = this.drawable.getContext().getGLDrawable().getWidth();
int w = compW / getCylindricalImagesCount();
int h = cylinderSectionHeight(w);
float aspect = (float) w / h;
double va = Math.min(fov, MAX_CYLINDER_FOV);
glu_callback_only.gluPerspective(va, aspect, Z_NEAR, Z_FAR);
break;
}
default:
float w = aspectRatio_callback_only * viewDistance;
float h = viewDistance;
gl.glOrtho(-w / 2, w / 2, -h / 2, h / 2, 0/*Z_NEAR*/, Z_FAR);
}
gl.glScalef(1.0f, -1.0f, 1.0f); // this inverts the y coordinates of the grid. A MESS! FIXME
}
/**
* GLEventListener Called by the drawable during the first repaint after the
* component has been resized.
*
* @param drawable
* @param x
* @param y
* @param w
* @param h
*/
@Override
public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {
// avoid division by zero
if (h == 0) {
h = 1;
}
GL gl1 = drawable.getGL();
GL2 gl = (GL2)gl1;
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glViewport(x, y, w, h);
this.aspectRatio_callback_only = (float) w / (float) h;
this.setupProjection(gl); // is this needed ? FIXME
}
void shearMatrix(GL gl1, float shearX, float shearY) {
GL2 gl = (GL2)gl1;
float m[] = {
1, 0, 0, 0, // col 1
0, 1, 0, 0, // col 2
shearX, shearY, 1, 0, // col 3
0, 0, 0, 1 // col 4
};
gl.glMultMatrixf(m, 0);
}
/**
* Renders the map.
*/
public void display() {
if (this.model.isInitialized()) {
this.updateView();
}
}
/**
* Set the background color. The color components are expected to be between
* 0 and 255.
*
* @param red the red color component
* @param green the green color component
* @param blue the blue color component
*/
public void setBackgroundColor(int red, int green, int blue) {
this.bgRed = red / 255.0f;
this.bgGreen = green / 255.0f;
this.bgBlue = blue / 255.0f;
this.component.repaint();
}
public void setBackgroundColor(Color bc) {
this.bgRed = bc.getRed() / 255.0f;
this.bgGreen = bc.getGreen() / 255.0f;
this.bgBlue = bc.getBlue() / 255.0f;
this.component.repaint();
}
public Color getBackgroundColor() {
return new Color((int) (bgRed * 255), (int) (bgGreen * 255), (int) (bgBlue * 255));
}
public void resetToDefaultCamera() {
this.setXAngle(defaultXAngle);
this.setZAngle(defaultZAngle);
this.setViewDistance(defaultDistance);
this.setViewAngle(defaultViewAngle);
this.setShearX(0);
this.setShearY(0);
this.setShiftX(defaultShiftX);
this.setShiftY(defaultShiftY);
this.setCylindricalHeight(defaultCylindricalHeight);
this.component.repaint();
}
public void setDefaultCamera(float defaultXAngle,
float defaultZAngle,
float defaultDistance,
float defaultViewAngle,
float defaultShiftX,
float defaultShiftY) {
this.defaultXAngle = defaultXAngle;
this.defaultZAngle = defaultZAngle;
this.defaultDistance = defaultDistance;
this.defaultViewAngle = defaultViewAngle;
this.defaultShiftX = defaultShiftX;
this.defaultShiftY = defaultShiftY;
}
public void setDefaultCylindricalCameraHeight(float defaultCylindricalHeight) {
this.defaultCylindricalHeight = defaultCylindricalHeight;
}
public Map3DAnimation getAnimation() {
return animation;
}
public void setAnimation(Map3DAnimation animation) {
if (animation == this.animation) {
return;
}
if (this.animation != null) {
this.animation.stopAnimation();
}
this.animation = animation;
this.component.addKeyListener(animation);
}
/**
* Rotation angle around x axis.
*
* @return Angle in degrees
*/
public float getXAngle() {
return xAngle;
}
/**
*
* @param xAngle in degrees
*/
public void setXAngle(float xAngle) {
xAngle = Math.max(Math.min(xAngle, MAX_X_ANGLE), MIN_X_ANGLE);
if (xAngle != this.xAngle) {
this.xAngle = xAngle;
this.updateView();
this.getComponent().firePropertyChange("view", 0, 1);
}
}
/**
* Returns the rotation angle around the z axis.
*
* @return Angle in degrees.
*/
public float getZAngle() {
return zAngle;
}
/**
*
* @param zAngle Angle in degrees.
* @return
*/
public static float normalizeZAngle(float zAngle) {
while (zAngle > 180) {
zAngle -= 360;
}
while (zAngle < -180) {
zAngle += 360;
}
return zAngle;
}
/**
* Rotation around vertical z axis.
*
* @param zAngle Angle in degrees.
*/
public void setZAngle(float zAngle) {
zAngle = normalizeZAngle(zAngle);
if (zAngle != this.zAngle) {
this.zAngle = zAngle;
this.updateView();
this.getComponent().firePropertyChange("view", 0, 1);
}
}
public float getViewDistance() {
return viewDistance;
}
public void setViewDistance(float viewDistance) {
viewDistance = Math.min(Math.max(viewDistance, MIN_DISTANCE), MAX_DISTANCE);
if (viewDistance != this.viewDistance) {
this.viewDistance = viewDistance;
this.updateView();
this.getComponent().firePropertyChange("view", 0, 1);
}
}
public float getViewAngle() {
return fov;
}
public void setViewAngle(float viewAngle) {
if (viewAngle != this.fov) {
this.fov = viewAngle;
this.updateView();
this.getComponent().firePropertyChange("view", 0, 1);
}
}
public float getShearXAngle() {
float angle = (float) (Math.toDegrees(Math.atan2(1, shearX)));
return Math.abs(angle) < 0.000001 ? 0f : angle;
}
public void setShearXAngle(float shearXAngle) {
shearXAngle = Math.min(Math.max(shearXAngle, MIN_SHEAR_ANGLE), MAX_SHEAR_ANGLE);
this.setShearX((float) (1. / Math.tan(shearXAngle)));
}
public float getShearX() {
return shearX;
}
public void setShearX(float shearX) {
if (this.shearX != shearX) {
this.shearX = shearX;
this.updateView();
this.getComponent().firePropertyChange("view", 0, 1);
}
}
public float getShearYAngle() {
float angle = (float) (Math.toDegrees(Math.atan2(1, shearY)));
return Math.abs(angle) < 0.000001 ? 0f : angle;
}
public void setShearYAngle(float shearYAngle) {
shearYAngle = Math.min(Math.max(shearYAngle, MIN_SHEAR_ANGLE), MAX_SHEAR_ANGLE);
this.setShearY((float) (1. / Math.tan(Math.toRadians(shearYAngle))));
}
public float getShearY() {
return shearY;
}
public void setShearY(float shearY) {
if (this.shearY != shearY) {
this.shearY = shearY;
this.updateView();
this.getComponent().firePropertyChange("view", 0, 1);
}
}
public float getShearDirection() {
return (float) Math.toDegrees(Math.atan2(shearY, shearX));
}
public float getShearRadius() {
return (float) Math.hypot(shearY, shearX);
}
public void setCamera(Camera camera) {
if (this.camera != camera) {
this.camera = camera;
this.updateView();
getComponent().firePropertyChange("camera changed", 0, 1);
}
}
public Camera getCamera() {
return camera;
}
public boolean is2D() {
return camera == Camera.orthogonal;
}
public float getShiftX() {
return shiftX;
}
public void setShiftX(float shiftX) {
if (this.shiftX != shiftX) {
this.shiftX = shiftX;
this.updateView();
this.getComponent().firePropertyChange("shiftX", 0, 1);
}
}
public float getShiftY() {
return shiftY;
}
public void setShiftY(float shiftY) {
if (this.shiftY != shiftY) {
this.shiftY = shiftY;
this.updateView();
this.getComponent().firePropertyChange("shiftY", 0, 1);
}
}
public void setShift(float shiftX, float shiftY) {
if (this.shiftX != shiftX || this.shiftY != shiftY) {
this.shiftX = shiftX;
this.shiftY = shiftY;
this.updateView();
this.getComponent().firePropertyChange("view", 0, 1);
}
}
/**
* @return the cylindricalHeight
*/
public float getCylindricalHeight() {
return cylindricalHeight;
}
/**
* @param cylindricalHeight the cylindricalHeight to set
*/
public void setCylindricalHeight(float h) {
if (this.cylindricalHeight != h) {
this.cylindricalHeight = h;
this.updateView();
this.getComponent().firePropertyChange("height", 0, 1);
}
}
private void updateView() {
this.component.repaint();
}
/**
* @return the fogEnabled
*/
public boolean isFogEnabled() {
return fogEnabled;
}
/**
* @param fogEnabled the fogEnabled to set
*/
public void setFogEnabled(boolean fogEnabled) {
this.fogEnabled = fogEnabled;
this.updateView();
this.getComponent().firePropertyChange("fog", 0, 1);
}
/**
* @return the fogStart
*/
public float getFogStart() {
return fogStart;
}
/**
* @param fogStart the fogStart to set
*/
public void setFogStart(float fogStart) {
this.fogStart = fogStart;
this.updateView();
this.getComponent().firePropertyChange("fogStart", 0, 1);
}
/**
* @return the fogEnd
*/
public float getFogEnd() {
return fogEnd;
}
/**
* @param fogEnd the fogEnd to set
*/
public void setFogEnd(float fogEnd) {
this.fogEnd = fogEnd;
this.updateView();
this.getComponent().firePropertyChange("fogEnd", 0, 1);
}
/**
* @return the fogColor
*/
public Color getFogColor() {
return fogColor;
}
/**
* @param fogColor the fogColor to set
*/
public void setFogColor(Color fogColor) {
this.fogColor = fogColor;
this.updateView();
this.getComponent().firePropertyChange("fogColor", 0, 1);
}
@Override
public void dispose(GLAutoDrawable arg0) {
// TODO Auto-generated method stub
}
/**
* FIXME: Hackish method for taking a mouse position and mapping
* it to a normalized location on the model.
* (a Point2D.Float with x & y values between [0.0-1.0])
* TODO: handle screen-to-map transformation when rotated
*/
protected Point2D.Float mouseXYtoModelXY(float x, float y){
int componentWidth = getComponent().getWidth();
int componentHeight = getComponent().getHeight();
float normalizedX = x / componentWidth;
float normalizedY = y / componentHeight;
float whRatio = (float)componentWidth / componentHeight;
float mapX = (normalizedX - 0.5f) * getViewDistance()*whRatio + 0.5f - getShiftX();
float mapY = (normalizedY - 0.5f) * getViewDistance() + 0.5f - getShiftY();
return new Point2D.Float(mapX, mapY);
}
/**
* Find the model coordinates corresponding to current viewport.
* FIXME: Depends on mouseXYtoModelXY, which only works with no rotations.
*/
public Rectangle2D.Float getViewBounds(){
int componentWidth = getComponent().getWidth();
int componentHeight = getComponent().getHeight();
Point2D.Float upperLeft = mouseXYtoModelXY(0,0);
Point2D.Float lowerRight = mouseXYtoModelXY(componentWidth, componentHeight);
return new Rectangle2D.Float(upperLeft.x, upperLeft.y,
lowerRight.x - upperLeft.x, lowerRight.y - upperLeft.y);
}
}