// Copyright 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.android.stardroid.control;
import static com.google.android.stardroid.util.Geometry.addVectors;
import static com.google.android.stardroid.util.Geometry.calculateRADecOfZenith;
import static com.google.android.stardroid.util.Geometry.matrixMultiply;
import static com.google.android.stardroid.util.Geometry.matrixVectorMultiply;
import static com.google.android.stardroid.util.Geometry.scalarProduct;
import static com.google.android.stardroid.util.Geometry.scaleVector;
import static com.google.android.stardroid.util.Geometry.vectorProduct;
import com.google.android.stardroid.ApplicationConstants;
import com.google.android.stardroid.units.GeocentricCoordinates;
import com.google.android.stardroid.units.LatLong;
import com.google.android.stardroid.units.Matrix33;
import com.google.android.stardroid.units.RaDec;
import com.google.android.stardroid.units.Vector3;
import com.google.android.stardroid.util.Geometry;
import com.google.android.stardroid.util.MiscUtil;
import java.util.Date;
/**
* The model of the astronomer.
*
* <p>Stores all the data about where and when he is and where he's looking and
* handles translations between three frames of reference:
* <ol>
* <li>Celestial - a frame fixed against the background stars with
* x, y, z axes pointing to (RA = 90, DEC = 0), (RA = 0, DEC = 0), DEC = 90
* <li>Phone - a frame fixed in the phone with x across the short side, y across
* the long side, and z coming out of the phone screen.
* <li>Local - a frame fixed in the astronomer's local position. x is due east
* along the ground y is due north along the ground, and z points towards the
* zenith.
* </ol>
*
* <p>We calculate the local frame in phone coords, and in celestial coords and
* calculate a transform between the two.
* In the following, N, E, U correspond to the local
* North, East and Up vectors (ie N, E along the ground, Up to the Zenith)
*
* <p>In Phone Space: axesPhone = [N, E, U]
*
* <p>In Celestial Space: axesSpace = [N, E, U]
*
* <p>We find T such that axesCelestial = T * axesPhone
*
* <p>Then, [viewDir, viewUp]_celestial = T * [viewDir, viewUp]_phone
*
* <p>where the latter vector is trivial to calculate.
*
* <p>Implementation note: this class isn't making defensive copies and
* so is vulnerable to clients changing its internal state.
*
* @author John Taylor
*/
public class AstronomerModelImpl implements AstronomerModel {
private static final String TAG = MiscUtil.getTag(AstronomerModelImpl.class);
private static final Vector3 POINTING_DIR_IN_PHONE_COORDS = new Vector3(0, 0, -1);
private static final Vector3 SCREEN_UP_IN_PHONE_COORDS = new Vector3(0, 1, 0);
private static final Vector3 AXIS_OF_EARTHS_ROTATION = new Vector3(0, 0, 1);
private static final long MINIMUM_TIME_BETWEEN_CELESTIAL_COORD_UPDATES_MILLIS = 60000L;
private MagneticDeclinationCalculator magneticDeclinationCalculator;
private boolean autoUpdatePointing = true;
private float fieldOfView = 45; // Degrees
private LatLong location = new LatLong(0f, 0f);
private Clock clock = new RealClock();
private long celestialCoordsLastUpdated = -1;
/**
* The pointing comprises a vector into the phone's screen expressed in
* celestial coordinates combined with a perpendicular vector along the
* phone's longer side.
*/
private Pointing pointing = new Pointing();
/** The sensor acceleration in the phone's coordinate system. */
private Vector3 acceleration = ApplicationConstants.INITIAL_DOWN;
/** The sensor magnetic field in the phone's coordinate system. */
private Vector3 magneticField = ApplicationConstants.INITIAL_SOUTH;
/** North along the ground in celestial coordinates. */
private Vector3 trueNorthCelestial = new Vector3(1, 0, 0);
/** Up in celestial coordinates. */
private Vector3 upCelestial = new Vector3(0, 1, 0);
/** East in celestial coordinates. */
private Vector3 trueEastCelestial = AXIS_OF_EARTHS_ROTATION;
/** [North, Up, East]^-1 in phone coordinates. */
private Matrix33 axesPhoneInverseMatrix = Matrix33.getIdMatrix();
/** [North, Up, East] in celestial coordinates. */
private Matrix33 axesMagneticCelestialMatrix = Matrix33.getIdMatrix();
/**
* @param magneticDeclinationCalculator A calculator that will provide the
* magnetic correction from True North to Magnetic North.
*/
public AstronomerModelImpl(MagneticDeclinationCalculator magneticDeclinationCalculator) {
setMagneticDeclinationCalculator(magneticDeclinationCalculator);
}
@Override
public void setAutoUpdatePointing(boolean autoUpdatePointing) {
this.autoUpdatePointing = autoUpdatePointing;
}
@Override
public float getFieldOfView() {
return fieldOfView;
}
@Override
public void setFieldOfView(float degrees) {
fieldOfView = degrees;
}
@Override
public Date getTime() {
return new Date(clock.getTimeInMillisSinceEpoch());
}
@Override
public LatLong getLocation() {
return location;
}
@Override
public void setLocation(LatLong location) {
this.location = location;
calculateLocalNorthAndUpInCelestialCoords(true);
}
@Override
public Vector3 getPhoneAcceleration() {
return acceleration;
}
@Override
public void setPhoneSensorValues(Vector3 acceleration, Vector3 magneticField) {
this.acceleration.assign(acceleration);
this.magneticField.assign(magneticField);
}
@Override
public GeocentricCoordinates getNorth() {
calculateLocalNorthAndUpInCelestialCoords(false);
return GeocentricCoordinates.getInstanceFromVector3(trueNorthCelestial);
}
@Override
public GeocentricCoordinates getSouth() {
calculateLocalNorthAndUpInCelestialCoords(false);
return GeocentricCoordinates.getInstanceFromVector3(Geometry.scaleVector(trueNorthCelestial,
-1));
}
@Override
public GeocentricCoordinates getZenith() {
calculateLocalNorthAndUpInCelestialCoords(false);
return GeocentricCoordinates.getInstanceFromVector3(upCelestial);
}
@Override
public GeocentricCoordinates getNadir() {
calculateLocalNorthAndUpInCelestialCoords(false);
return GeocentricCoordinates.getInstanceFromVector3(Geometry.scaleVector(upCelestial, -1));
}
@Override
public GeocentricCoordinates getEast() {
calculateLocalNorthAndUpInCelestialCoords(false);
return GeocentricCoordinates.getInstanceFromVector3(trueEastCelestial);
}
@Override
public GeocentricCoordinates getWest() {
calculateLocalNorthAndUpInCelestialCoords(false);
return GeocentricCoordinates.getInstanceFromVector3(Geometry.scaleVector(trueEastCelestial,
-1));
}
@Override
public void setMagneticDeclinationCalculator(MagneticDeclinationCalculator calculator) {
this.magneticDeclinationCalculator = calculator;
calculateLocalNorthAndUpInCelestialCoords(true);
}
/**
* Updates the astronomer's 'pointing', that is, the direction the phone is
* facing in celestial coordinates and also the 'up' vector along the
* screen (also in celestial coordinates).
*
* <p>This method requires that {@link #axesMagneticCelestialMatrix} and
* {@link #axesPhoneInverseMatrix} are currently up to date.
*/
private void calculatePointing() {
if (!autoUpdatePointing) {
return;
}
Matrix33 transform = matrixMultiply(axesMagneticCelestialMatrix, axesPhoneInverseMatrix);
Vector3 viewInSpaceSpace = matrixVectorMultiply(transform, POINTING_DIR_IN_PHONE_COORDS);
Vector3 screenUpInSpaceSpace = matrixVectorMultiply(transform, SCREEN_UP_IN_PHONE_COORDS);
pointing.updateLineOfSight(viewInSpaceSpace);
pointing.updatePerpendicular(screenUpInSpaceSpace);
}
/**
* Calculates local North, East and Up vectors in terms of the celestial
* coordinate frame.
*/
private void calculateLocalNorthAndUpInCelestialCoords(boolean forceUpdate) {
long currentTime = clock.getTimeInMillisSinceEpoch();
if (!forceUpdate &&
Math.abs(currentTime - celestialCoordsLastUpdated) <
MINIMUM_TIME_BETWEEN_CELESTIAL_COORD_UPDATES_MILLIS) {
return;
}
celestialCoordsLastUpdated = currentTime;
updateMagneticCorrection();
RaDec up = calculateRADecOfZenith(getTime(), location);
upCelestial = GeocentricCoordinates.getInstance(up);
Vector3 z = AXIS_OF_EARTHS_ROTATION;
float zDotu = scalarProduct(upCelestial, z);
trueNorthCelestial = addVectors(z, scaleVector(upCelestial, -zDotu));
trueNorthCelestial.normalize();
trueEastCelestial = Geometry.vectorProduct(trueNorthCelestial, upCelestial);
// Apply magnetic correction. Rather than correct the phone's axes for
// the magnetic declination, it's more efficient to rotate the
// celestial axes by the same amount in the opposite direction.
Matrix33 rotationMatrix = Geometry.calculateRotationMatrix(
magneticDeclinationCalculator.getDeclination(), upCelestial);
Vector3 magneticNorthCelestial = Geometry.matrixVectorMultiply(rotationMatrix,
trueNorthCelestial);
Vector3 magneticEastCelestial = vectorProduct(magneticNorthCelestial, upCelestial);
axesMagneticCelestialMatrix = new Matrix33(magneticNorthCelestial,
upCelestial,
magneticEastCelestial);
}
/**
* Calculates local North and Up vectors in terms of the phone's coordinate
* frame.
*/
private void calculateLocalNorthAndUpInPhoneCoords() {
// TODO(johntaylor): we can reduce the number of vector copies done in here.
Vector3 down = acceleration.copy();
down.normalize();
// Magnetic field goes *from* North to South, so reverse it.
Vector3 magneticFieldToNorth = magneticField.copy();
magneticFieldToNorth.scale(-1);
magneticFieldToNorth.normalize();
// This is the vector to magnetic North *along the ground*.
Vector3 magneticNorthPhone = addVectors(magneticFieldToNorth,
scaleVector(down, -scalarProduct(magneticFieldToNorth, down)));
magneticNorthPhone.normalize();
Vector3 upPhone = scaleVector(down, -1);
Vector3 magneticEastPhone = vectorProduct(magneticNorthPhone, upPhone);
// The matrix is orthogonal, so transpose it to find its inverse.
// Easiest way to do that is to construct it from row vectors instead
// of column vectors.
axesPhoneInverseMatrix = new Matrix33(magneticNorthPhone, upPhone, magneticEastPhone, false);
}
/**
* Updates the angle between True North and Magnetic North.
*/
private void updateMagneticCorrection() {
magneticDeclinationCalculator.setLocationAndTime(location, getTimeMillis());
}
/**
* Returns the user's pointing. Note that clients should not usually modify this
* object as it is not defensively copied.
*/
@Override
public Pointing getPointing() {
calculateLocalNorthAndUpInPhoneCoords();
calculatePointing();
return pointing;
}
@Override
public void setPointing(Vector3 lineOfSight, Vector3 perpendicular) {
this.pointing.updateLineOfSight(lineOfSight);
this.pointing.updatePerpendicular(perpendicular);
}
@Override
public void setClock(Clock clock) {
this.clock = clock;
calculateLocalNorthAndUpInCelestialCoords(true);
}
@Override
public long getTimeMillis() {
return clock.getTimeInMillisSinceEpoch();
}
}