/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2013, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.display3d.animation;
import com.jogamp.opengl.GL;
import com.vividsolutions.jts.geom.Coordinate;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.geotoolkit.math.XMath;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import org.geotoolkit.display3d.Map3D;
import org.geotoolkit.display3d.phase.Phase;
import org.geotoolkit.display3d.scene.ContextContainer3D;
import org.geotoolkit.display3d.scene.Terrain;
import org.geotoolkit.display3d.scene.camera.TrackBallCamera;
import org.geotoolkit.display3d.utils.BezierCurve;
/**
*
* @author Thomas Rouby (Geomatys)
*/
public class Animation implements Phase {
private Map3D map3d;
private final List<List<Vector3d>> lineControlPoints = new ArrayList<>();
private final List<AnimationPhase> phases = new ArrayList<>();
private List<Vector3d> pathPoints;
private List<Double> pathLength = new ArrayList<>();
private double totalDistance = 0.0;
private double high;
private int precision = 10;
private double speed = 1.0;
private double scale;
private boolean isRun = false;
private boolean isValid = false;
private long curTime = 0l;
private long duration = 0l;
public Animation(Map3D map3d){
this(map3d,new ArrayList<Vector3d>(),0,0,10,1.0);
}
public Animation(Map3D map3d, List<Vector3d> pathPoints, double high, double scale, int precision){
this(map3d, pathPoints, high, scale, precision, 1.0);
}
public Animation(Map3D map3d, List<Vector3d> pathPoints, double high, double scale, double speed){
this(map3d, pathPoints, high, scale, 10, speed);
}
public Animation(Map3D map3d, List<Vector3d> pathPoints, double high, double scale){
this(map3d, pathPoints, high, scale, 10, 1.0);
}
public Animation(Map3D map3d, List<Vector3d> pathPoints, double high, double scale, int precision, double speed){
this.map3d = map3d;
this.pathPoints = new ArrayList<>(pathPoints);
this.high = high;
this.scale = scale;
this.precision = precision;
this.speed = speed;
}
public void addPhase(AnimationPhase phase){
this.phases.add(phase);
}
public void removePhase(AnimationPhase phase){
this.phases.remove(phase);
}
public List<AnimationPhase> getPhases(){
return phases;
}
public final double getHigh() {
return high;
}
public void setHigh(double high) {
this.high = high;
this.isValid = false;
}
public final double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
this.isValid = false;
}
public final double getScale() {
return scale;
}
public void setScale(double scale) {
this.scale = scale;
this.isValid = false;
}
public void setPathPoints(List<Coordinate> pathPoints) {
if (pathPoints != null){
this.pathPoints = new ArrayList<>();
for (Coordinate point : pathPoints){
this.pathPoints.add(new Vector3d(point.x, point.y, point.z));
}
}
this.isValid = false;
}
public boolean isRun() {
return this.isRun;
}
public boolean isValid(){
return this.isValid;
}
public boolean validate() {
try {
this.computeAll();
} catch (Exception ex) {
Map3D.LOGGER.log(Level.WARNING, ex.getMessage(), ex);
this.isValid = false;
}
return this.isValid;
}
public double approximateSpeed(long duration){
double length = 0.0;
for (int i=0; i<this.pathPoints.size()-2; i++){
Vector3d tmp1 = this.pathPoints.get(i);
Vector3d tmp2 = this.pathPoints.get(i+1);
Vector3d len = new Vector3d(0.0, 0.0, 0.0);
len.sub(tmp2, tmp1);
length += len.length();
}
return length / duration;
}
public void computeAll() throws AnimationNotValidException {
if (this.isRun){
return;
}
this.isValid = false;
if (this.pathPoints.size()<3) return;
this.computeAltitude();
// this.computeCollision();
this.computeControlPoints();
this.computeDistance();
this.isValid = true;
}
private void computeAltitude() throws AnimationNotValidException {
try {
for (Vector3d pathPoint : this.pathPoints){
pathPoint.z = this.high;
}
} catch (Exception ex) {
throw new AnimationNotValidException(ex);
}
}
// private void computeCollision() throws AnimationNotValidException {
// try {
// final Terrain terrain = this.map3d.getTerrain();
// if (terrain != null){
// final TrackBallCamera camera = this.map3d.getCamera();
// final double scale = camera.getViewScale(camera.getLength());
// int pathSize = this.pathPoints.size();
//
// for (int i=0; i<pathSize-1; i++){
// final Vector3d P0 = this.pathPoints.get(i);
// final Vector3d P1 = this.pathPoints.get(i+1);
//
// final Vector3d P0P1 = new Vector3d();
// P0P1.sub(P1, P0);
//
// for (int t=1; t<=this.precision; t++){
// final double pos = (double)t/(this.precision +1);
//
// final Vector3d testPoint = new Vector3d(P0.x+pos*P0P1.x, P0.y+pos*P0P1.y, P0.z+pos*P0P1.z);
// final double altitude = terrain.getAltitudeOf(testPoint.x, testPoint.y, this.scale);
//
// if (testPoint.z < altitude){
// testPoint.z = altitude+this.high;
// this.pathPoints.add(i+1, testPoint);
// pathSize += 1;
// break;
// }
// }
// }
// }
// } catch (Exception ex) {
// throw new AnimationNotValidException(ex);
// }
// }
private static Vector3d tangeante3Points(Vector3d P1, Vector3d P2, Vector3d P3){
final Vector3d P2P3 = new Vector3d();
P2P3.sub(P3,P2);
P2P3.normalize();
final Vector3d P2P1 = new Vector3d();
P2P1.sub(P1, P2);
P2P1.normalize();
final Vector3d T1 = new Vector3d();
T1.add(P2, P2P1);
final Vector3d T2 = new Vector3d();
T2.add(P2, P2P3);
final Vector3d T1T2 = new Vector3d();
T1T2.sub(T2, T1);
return T1T2;
}
private void computeControlPoints() throws AnimationNotValidException {
this.lineControlPoints.clear();
for (int i=0; i<this.pathPoints.size()-1; i++){
List<Vector3d> controlPoints = new ArrayList<>();
Vector3d P1 = this.pathPoints.get(i);
Vector3d P2 = this.pathPoints.get(i+1);
Vector3d P1P2 = new Vector3d();
P1P2.sub(P2,P1);
if (this.pathPoints.size() > 2){
try {
Vector3d tan1;
Vector3d tan2;
if (i == 0){
Vector3d P3 = this.pathPoints.get(i+2);
tan1 = new Vector3d(P1P2);
tan2 = tangeante3Points(P1, P2, P3);
} else if (i == this.pathPoints.size()-2){
Vector3d P0 = this.pathPoints.get(i-1);
tan1 = tangeante3Points(P0, P1, P2);
tan2 = new Vector3d(P1P2);
} else {
Vector3d P0 = this.pathPoints.get(i-1);
Vector3d P3 = this.pathPoints.get(i+2);
tan1 = tangeante3Points(P0, P1, P2);
tan2 = tangeante3Points(P1, P2, P3);
}
tan1.normalize();
tan2.normalize();
Vector3d M1 = new Vector3d(P1P2);
M1.scale(1.0/5.0);
M1.add(P1, M1);
Vector3d M2 = new Vector3d(P1P2);
M2.scale(4.0/5.0);
M2.add(P1, M2);
double a1 = P2.x*tan1.x - P1.x*tan1.x +
P2.y*tan1.y - P1.y*tan1.y +
P2.z*tan1.z - P1.z*tan1.z;
double b1 = P2.x*P1.x - P2.x*M1.x - P1.x*P1.x + P1.x*M1.x +
P2.y*P1.y - P2.y*M1.y - P1.y*P1.y + P1.y*M1.y +
P2.z*P1.z - P2.z*M1.z - P1.z*P1.z + P1.z*M1.z;
double t1 = -b1/a1; // GeometryUtils.clamp(-b1 / a1, -this.speed, this.speed);
final double xo1 = P1.x + t1*tan1.x;
final double yo1 = P1.y + t1*tan1.y;
final double zo1 = P1.z + t1*tan1.z;
Vector3d C1 = new Vector3d(xo1, yo1, zo1);
// if (terrain != null){
// double altitude1 = terrain.getAltitudeOf(C1.x, C1.y, this.scale);
// while (C1.z <= altitude1){
// t1 /= 2;
// C1.x = P1.x + t1*tan1.x;
// C1.y = P1.y + t1*tan1.y;
// C1.z = P1.z + t1*tan1.z;
// altitude1 = terrain.getAltitudeOf(C1.x, C1.y, this.scale);
// }
// }
double a2 = P2.x*tan2.x - P1.x*tan2.x +
P2.y*tan2.y - P1.y*tan2.y +
P2.z*tan2.x - P1.z*tan2.z;
double b2 = P2.x*P2.x - P2.x*M2.x - P1.x*P2.x + P1.x*M2.x +
P2.y*P2.y - P2.y*M2.y - P1.y*P2.y + P1.y*M2.y +
P2.z*P2.z - P2.z*M2.z - P1.z*P2.z + P1.z*M2.z;
double t2 = -b2 / a2; // GeometryUtils.clamp(-b2 / a2, -this.speed, this.speed);
final double xo2 = P2.x + t2*tan2.x;
final double yo2 = P2.y + t2*tan2.y;
final double zo2 = P2.z + t2*tan2.z;
Vector3d C2 = new Vector3d(xo2, yo2, zo2);
// if (terrain != null){
// double altitude2 = terrain.getAltitudeOf(C2.x, C2.y, this.scale);
// while (C2.z <= altitude2){
// t2 /= 2;
// C2.x = P2.x + t2*tan2.x;
// C2.y = P2.y + t2*tan2.y;
// C2.z = P2.z + t2*tan2.z;
// altitude2 = terrain.getAltitudeOf(C2.x, C2.y, this.scale);
// }
// }
controlPoints.add(C1);
controlPoints.add(C2);
} catch (Exception ex) {
throw new AnimationNotValidException(ex);
}
}
this.lineControlPoints.add(controlPoints);
}
}
private void computeDistance(){
this.totalDistance = 0.0;
this.pathLength = new ArrayList<Double>();
for (int i=0; i<this.pathPoints.size()-1; i++){
final List<Vector3d> bezierPath = new ArrayList<Vector3d>();
final List<Vector3d> controlPoints = this.lineControlPoints.get(i);
final Vector3d P0 = this.pathPoints.get(i);
final Vector3d P1 = this.pathPoints.get(i+1);
bezierPath.add(P0);
for (int j=1; j<=this.precision; j++){
final double t = (double)j/(double)(this.precision+1);
final List<Vector3d> Pi = new ArrayList<>();
Pi.add(P0);
Pi.add(controlPoints.get(0));
Pi.add(controlPoints.get(1));
Pi.add(P1);
final Vector3d curve = BezierCurve.bezierCurve(Pi, t);
bezierPath.add(curve);
}
bezierPath.add(P1);
double length = 0.0;
for (int b=0; b<bezierPath.size()-1; b++){
final Vector3d V0 = bezierPath.get(b);
final Vector3d V1 = bezierPath.get(b+1);
final Vector3d V0V1 = new Vector3d();
V0V1.sub(V1, V0);
length += V0V1.length();
}
this.totalDistance += length;
this.pathLength.add(length);
}
}
private double distanceAt(double time, double totalTime){
return this.totalDistance*XMath.clamp(time/totalTime, 0.0, 1.0);
}
public Vector3d accelAt(double time, double totalTime) throws AnimationNotValidException {
return this.accelAt(this.distanceAt(time, totalTime));
}
public Vector3d accelAt(double distance) throws AnimationNotValidException {
this.assertIsValid();
int index = -1;
double t = 0.0;
Vector3d P1 = new Vector3d();
Vector3d P2 = new Vector3d();
Vector3d P1P2 = new Vector3d();
double compDist = 0.0;
for (int i=0; i<this.pathLength.size(); i++){
P1 = this.pathPoints.get(i);
P2 = this.pathPoints.get(i+1);
P1P2.sub(P2, P1);
index = i;
t = XMath.clamp((distance - compDist) / this.pathLength.get(i), 0.0, 1.0);
if (compDist+this.pathLength.get(i) > distance){
break;
}
compDist += this.pathLength.get(i);
}
final List<Vector3d> controlPoints = this.lineControlPoints.get(index);
if (controlPoints.size() == 2){
final List<Vector3d> Pi = new ArrayList<>();
Pi.add(P1);
Pi.add(controlPoints.get(0));
Pi.add(controlPoints.get(1));
Pi.add(P2);
return BezierCurve.bezierSecondCurve(Pi, t);
}
return null;
}
public Vector3d speedAt(double time, double totalTime) throws AnimationNotValidException {
return this.speedAt(this.distanceAt(time, totalTime));
}
public Vector3d speedAt(double distance) throws AnimationNotValidException {
this.assertIsValid();
int index = -1;
double t = 0.0;
Vector3d P1 = new Vector3d();
Vector3d P2 = new Vector3d();
Vector3d P1P2 = new Vector3d();
double compDist = 0.0;
for (int i=0; i<this.pathLength.size(); i++){
P1 = this.pathPoints.get(i);
P2 = this.pathPoints.get(i+1);
P1P2.sub(P2, P1);
index = i;
t = XMath.clamp((distance-compDist)/this.pathLength.get(i), 0.0, 1.0);
if (compDist+this.pathLength.get(i) > distance){
break;
}
compDist += this.pathLength.get(i);
}
final List<Vector3d> controlPoints = this.lineControlPoints.get(index);
if (controlPoints.size() == 2){
final List<Vector3d> Pi = new ArrayList<>();
Pi.add(P1);
Pi.add(controlPoints.get(0));
Pi.add(controlPoints.get(1));
Pi.add(P2);
return BezierCurve.bezierDerivativeCurve(Pi, t);
}
return null;
}
public Vector3d positionAt(double time, double totalTime) throws AnimationNotValidException {
return this.positionAt(this.distanceAt(time, totalTime));
}
public Vector3d positionAt(double distance) throws AnimationNotValidException {
this.assertIsValid();
int index = -1;
double t = 0.0;
Vector3d P1 = new Vector3d();
Vector3d P2 = new Vector3d();
Vector3d P1P2 = new Vector3d();
double compDist = 0.0;
for (int i=0; i<this.pathLength.size(); i++){
P1 = this.pathPoints.get(i);
P2 = this.pathPoints.get(i+1);
P1P2.sub(P2, P1);
index = i;
t = XMath.clamp((distance-compDist)/this.pathLength.get(i), 0.0, 1.0);
if (compDist+this.pathLength.get(i) > distance){
break;
}
compDist += this.pathLength.get(i);
}
final List<Vector3d> controlPoints = this.lineControlPoints.get(index);
if (controlPoints.size() == 2){
final List<Vector3d> Pi = new ArrayList<>();
Pi.add(P1);
Pi.add(controlPoints.get(0));
Pi.add(controlPoints.get(1));
Pi.add(P2);
return BezierCurve.bezierCurve(Pi, t);
}
return null;
}
public List<Vector3d> getPathPoints() {
final List<Vector3d> tmpPathPoints = new ArrayList<Vector3d>();
for (Vector3d point : this.pathPoints){
tmpPathPoints.add(new Vector3d(point.x, point.y, point.z));
}
return tmpPathPoints;
}
public List<Vector3d> getControlPoints() {
final List<Vector3d> controls = new ArrayList<Vector3d>();
for (int i=0; i<this.pathPoints.size(); i++){
controls.add(new Vector3d(this.pathPoints.get(i)));
if (i < this.lineControlPoints.size()){
final List<Vector3d> controlPoints = this.lineControlPoints.get(i);
for (int j=0; j<controlPoints.size(); j++){
controls.add(new Vector3d(controlPoints.get(j).x, controlPoints.get(j).y, controlPoints.get(j).z));
}
}
}
return controls;
}
public void assertIsValid() throws AnimationNotValidException {
if (!this.isValid){
throw new AnimationNotValidException();
}
}
public void initializeAnimationPosition() throws AnimationNotValidException {
if (!this.isValid){
throw new AnimationNotValidException();
}
final Terrain terrain = ((ContextContainer3D)map3d.getContainer()).getTerrain();
if (terrain != null){
try {
final Vector3d position = this.positionAt(0.0);
GeneralDirectPosition pos = new GeneralDirectPosition(terrain.getEnvelope().getCoordinateReferenceSystem());
pos.setOrdinate(0, position.x);
pos.setOrdinate(1, position.y);
final double alti = terrain.getAltitudeSmoothOf(pos, terrain.getMaxScale());
position.z += alti;
final Vector3d direction = this.speedAt(0.0);
direction.z = 0.0;
direction.normalize();
double angle = 180.0 + Math.toDegrees(Math.acos(-direction.x));
if (direction.y != 0.0){
angle = 180.0f + Math.signum(direction.y)*Math.toDegrees(Math.acos(-direction.x));
}
this.map3d.getCamera().setRotateZ((float) angle + 90.0f);
this.map3d.getCamera().setCenter(new Vector3f(position));
} catch (Exception ex) {
throw new AnimationNotValidException(ex);
}
}
}
public void startAnimation(long duration) throws AnimationNotValidException {
this.isRun = true;
this.duration = duration;
for (AnimationPhase phase : phases){
phase.beginPhase(this);
}
map3d.addPhase(this);
}
public void stopAnimation() {
map3d.removePhase(this);
this.isRun = false;
for (AnimationPhase phase : phases){
phase.endPhase(this);
}
}
public long getDuration() {
return duration;
}
public long getCurTime(){
return curTime;
}
public void setCurTime(long position){
this.curTime = XMath.clamp(position, 0l, duration);
}
@Override
public void setMap(Map3D map) {
this.map3d = map3d;
}
@Override
public Map3D getMap() {
return this.map3d;
}
@Override
public void update(GL gl) {
if (this.isRun) {
try{
final TrackBallCamera camera = this.map3d.getCamera();
final Terrain terrain = ((ContextContainer3D)map3d.getContainer()).getTerrain();
final Vector3d position = this.positionAt(curTime, duration);
GeneralDirectPosition pos = new GeneralDirectPosition(terrain.getEnvelope().getCoordinateReferenceSystem());
pos.setOrdinate(0, position.x);
pos.setOrdinate(1, position.y);
final double alti = terrain.getAltitudeSmoothOf(pos, terrain.getMaxScale());
position.z += alti;
final Vector3d direction = this.speedAt(curTime, duration);
direction.z = 0.0;
direction.normalize();
final double angle;
if (Math.signum(direction.y) != 0.0){
angle = 180.0f + Math.signum(direction.y)*Math.toDegrees(Math.acos(-direction.x));
} else {
angle = 180.0 + Math.toDegrees(Math.acos(-direction.x));
}
camera.setRotateZ((float) angle + 90.0f);
camera.setCenter(new Vector3f(position));
} catch (Exception ex) {
this.map3d.getMonitor().exceptionOccured(ex, Level.WARNING);
}
for (AnimationPhase phase : phases){
phase.currentPhase(gl, this);
}
if (curTime >= duration) {
this.stopAnimation();
}
}
}
public interface AnimationPhase {
public void beginPhase(Animation animation);
public void currentPhase(GL gl, Animation animation);
public void endPhase(Animation animation);
}
}