(*
Augmented Reality Toolkit for Delphi
based on the iPhone Augmented Reality Toolkit
Copyright (c) 2013 Agilite Software
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*)
unit UARKit;
interface uses Classes, SysUtils;
const
ADJUST_BY = 30;
SCALE_FACTOR = 1.0;
DEGREE_TO_UPDATE = 1;
HEADING_NOT_SET = -1.0;
type
TARCameraOrientation = (
coLandscapeLeft, coLandscapeRight,
coPortrait, coPortraitUpsideDown);
TARList = TList;
TARPoint =
record x, y: single;
end;
TARRect =
record Left, Top, Right, Bottom: single;
end;
TARLocation =
record Latitude, Longitude, Altitude: single;
end;
TARVector =
record x, y, z: single;
end;
TARMatrix =
record
m11, m12, m13, m14: single;
m21, m22, m23, m24: single;
m31, m32, m33, m34: single;
m41, m42, m43, m44: single;
end;
const
ARIdentityMatrix: TARMatrix = (
m11: 1; m12: 0; m13: 0; m14: 0;
m21: 0; m22: 1; m23: 0; m24: 0;
m31: 0; m32: 0; m33: 1; m34: 0;
m41: 0; m42: 0; m43: 0; m44: 1);
type
TARCoordinate =
class
private
FTitle:
string;
FSubtitle:
string;
FAzimuth: single;
FInclination: single;
FRadialDistance: single;
public
constructor Create(
const ARadialDistance, AInclination, AAzimuth: single);
property Azimuth: single
read FAzimuth
write FAzimuth;
property Inclination: single
read FInclination
write FInclination;
property RadialDistance: single
read FRadialDistance
write FRadialDistance;
property Title:
string read FTitle
write FTitle;
property Subtitle:
string read FSubtitle
write FSubtitle;
end;
TARGeoCoordinate =
class(TARCoordinate)
private
FDisplayView: TARRect;
FGeoLocation: TARLocation;
FDistanceFromOrigin: single;
FWidth: integer;
FHeight: integer;
function AngleFromCoordinate(
const first, second: TARLocation): single;
public
constructor Create(
const ALocation: TARLocation;
const ATitle:
string);
overload;
constructor Create(
const AOrigin: TARGeoCoordinate);
overload;
procedure CalibrateUsingOrigin(
const AOrigin: TARLocation);
property Width: integer
read FWidth
write FWidth;
property Height: integer
read FHeight
write FHeight;
property DisplayView: TARRect
read FDisplayView
write FDisplayView;
property GeoLocation: TARLocation
read FGeoLocation;
property DistanceFromOrigin: Single
read FDistanceFromOrigin;
end;
TARController =
class
private
FLatestHeading: single;
FDegreeRange: single;
FViewAngle: single;
FPrevHeading: single;
FDisplayView: TARRect;
FCoordinates: TARList;
FCameraOrientation: TARCameraOrientation;
FCenterCoordinate: TARCoordinate;
FCenterLocation: TARLocation;
FMaximumScaleDistance: single;
FMinimumScaleFactor: single;
FMaximumRotationAngle: single;
FScaleViewsBasedOnDistance: Boolean;
FRotateViewsBasedOnPerspective: Boolean;
procedure UpdateLocations();
function FindDeltaOfRadianCenter(centerAzimuth, pointAzimuth: single;
var isBetweenNorth: Boolean): single;
function PointForCoordinate(
const coord: TARcoordinate): TARPoint;
function ShouldDisplayCoordinate(
const coord: TARCoordinate): Boolean;
procedure SetCenterLocation(
const ALocation: TARLocation);
procedure SetDisplayView(
const ADisplayView: TARRect);
procedure SetScaleData(
const index: integer;
const AValue: Single);
procedure SetViewData(
const index: integer;
const AValue: Boolean);
public
constructor Create(
const ADisplayView: TARRect);
destructor Destroy;
override;
procedure Clear;
procedure Add(
const coordinate: TARCoordinate);
procedure Remove(
const coordinate: TARCoordinate);
procedure UpdateAccelerometer(
const Ax, Ay, Az: single);
procedure UpdateHeading(
const AHeading: single);
property CenterLocation: TARLocation
read FCenterLocation
write SetCenterLocation;
property DisplayView: TARRect
read FDisplayView
write SetDisplayView;
property MaximumScaleDistance: single
index 0
read FMaximumScaleDistance
write SetScaleData;
property MinimumScaleFactor: single
index 1
read FMinimumScaleFactor
write SetScaleData;
property MaximumRotationAngle: single
index 2
read FMaximumRotationAngle
write SetScaleData;
property ScaleViewsBasedOnDistance: Boolean
index 0
read FScaleViewsBasedOnDistance
write SetViewData;
property RotateViewsBasedOnPerspective: Boolean
index 1
read FRotateViewsBasedOnPerspective
write SetViewData;
end;
implementation uses Math;
function ToDeg(
const x: single): single;
begin
result := PI * x / 180.0;
end;
function toRad(
const x: single): single;
begin
result := x * 180.0 / PI;
end;
function ARTransform3DScale(
const Matrix: TARMatrix;
const ScaleX, ScaleY, ScaleZ: single): TARMatrix;
begin
result := Matrix;
result.m11 := ScaleX;
result.m22 := ScaleY;
result.m33 := ScaleZ;
result.m44 := 1;
end;
function ARRectWidth(
const R: TARRect): Single;
begin
result := R.Right - R.Left;
end;
function ARRectHeight(
const R: TARRect): Single;
begin
result := R.Bottom - R.Top;
end;
function ARVector(
const x, y, z: single): TARVector;
begin
result.x := x;
result.y := y;
result.z := z;
end;
function ARSetTransform(
const src: TARVector;
const m: TARMatrix): TARVector;
var w: single;
begin
result.x := src.x * m.m11 + src.y * m.m21 + src.z * m.m31 + m.m41;
result.y := src.x * m.m12 + src.y * m.m22 + src.z * m.m31 + m.m41;
result.z := src.x * m.m13 + src.y * m.m23 + src.z * m.m32 + m.m41;
w := src.x * m.m14 + src.y * m.m24 + src.z * m.m34 + m.m44;
if (w <> 1)
and (w <> 0)
then
begin
result.x := result.x / w;
result.y := result.y / w;
result.z := result.z / w;
end;
end;
procedure SetTransform(
var Rect: TARRect;
const Matrix: TARMatrix);
var v1, v2: TARVector;
begin
v1 := ARSetTransform(ArVector(Rect.Left, Rect.Top, 0), Matrix);
v2 := ARSetTransform(ArVector(Rect.Right, Rect.Bottom, 0), Matrix);
Rect.Left := v1.x;
Rect.Top := v1.y;
Rect.Right := v2.x;
Rect.Bottom := v2.y;
end;
function DistanceFromLocation(
const Location1, Location2: TARLocation): single;
// http://www.movable-type.co.uk/scripts/latlong.html
var lat1, lat2, lon1, lon2: single;
Haversine, dLat, dLon: single;
begin
lat1 := ToDeg(Location1.Latitude);
lon1 := ToDeg(Location1.Longitude);
lat2 := ToDeg(Location2.Latitude);
lon2 := ToDeg(Location2.Longitude);
dLat := (lat2 - lat1);
dLon := (lon2 - lon1);
Haversine := sin(dLat / 2) * sin(dLat / 2) +
sin(dLon / 2) * sin(dLon / 2) * cos(lat1) * cos(lat2);
result := 6371 * (2 * arctan2(sqrt(Haversine), sqrt(1 - Haversine)));
end;
{ TARCoordinate }
constructor TARCoordinate.create(
const ARadialDistance, AInclination, AAzimuth: single);
begin
FRadialDistance := ARadialDistance;
FInclination := AInclination;
FAzimuth := AAzimuth;
FTitle := '
';
FSubtitle := '
';
end;
{ TARGeoCoordinate }
function TARGeoCoordinate.angleFromCoordinate(
const first, second: TARLocation): single;
var longitudinalDifference, latitudinalDifference: single;
possibleAzimuth: single;
begin
longitudinalDifference := second.longitude - first.longitude;
latitudinalDifference := second.latitude - first.latitude;
possibleAzimuth := (PI * 0.5) - arctan(latitudinalDifference / longitudinalDifference);
if (longitudinalDifference > 0)
then
result := possibleAzimuth
else if (longitudinalDifference < 0)
then
result := possibleAzimuth + PI
else if (latitudinalDifference < 0)
then
result := PI
else
result := 0;
end;
constructor TARGeoCoordinate.create(
const ALocation: TARLocation;
const ATitle:
string);
begin
inherited create(0, 0, 0);
FGeoLocation := ALocation;
FTitle := Title;
end;
constructor TARGeoCoordinate.create(
const AOrigin: TARGeoCoordinate);
begin
inherited create(0, 0, 0);
calibrateUsingOrigin(AOrigin.GeoLocation);
FTitle := '
';
end;
procedure TARGeoCoordinate.calibrateUsingOrigin(
const AOrigin: TARLocation);
var angle: single;
begin
FDistanceFromOrigin := DistanceFromLocation(GeoLocation, AOrigin);
RadialDistance := sqrt(power(AOrigin.Altitude - geoLocation.altitude, 2) + power(distanceFromOrigin, 2));
angle := sin(ABS(Aorigin.altitude - geoLocation.altitude) / radialDistance);
if (Aorigin.altitude > geoLocation.altitude)
then
angle := -angle;
Inclination := angle;
Azimuth := angleFromCoordinate(AOrigin, geoLocation);
end;
{ TARController }
constructor TARController.create(
const ADisplayView: TARRect);
begin
FCenterCoordinate := TARCoordinate.Create(1.0, 0, 0);
FCoordinates := TARList.create;
FLatestHeading := HEADING_NOT_SET;
FPrevHeading := HEADING_NOT_SET;
FMaximumScaleDistance := 0.0;
FMinimumScaleFactor := SCALE_FACTOR;
FScaleViewsBasedOnDistance := false;
FRotateViewsBasedOnPerspective := false;
FMaximumRotationAngle := PI / 6.0;
FDisplayView := ADisplayView;
FDegreeRange := ARRectWidth(ADisplayView) / ADJUST_BY;
FCenterLocation.Latitude := 37.41711;
FCenterLocation.longitude := -122.02528;
end;
destructor TARController.Destroy;
begin
Clear;
FCoordinates.Free;
FCenterCoordinate.free;
inherited;
end;
procedure TARController.Clear;
var i: integer;
obj: TObject;
begin
for i := 0
to FCoordinates.count - 1
do
begin
obj := FCoordinates[i];
FreeAndNil(obj);
end;
FCoordinates.clear;
end;
procedure TARController.Add(
const coordinate: TARCoordinate);
begin
FCoordinates.Add(Coordinate);
end;
procedure TARController.Remove(
const coordinate: TARCoordinate);
begin
FCoordinates.Remove(coordinate)
end;
function TARController.findDeltaOfRadianCenter(centerAzimuth: single; pointAzimuth: single;
var isBetweenNorth: Boolean): single;
begin
if centerAzimuth < 0.0
then
centerAzimuth := 2 * PI + centerAzimuth;
if centerAzimuth > 2 * PI
then
centerAzimuth := centerAzimuth - 2 * PI;
result := abs(pointAzimuth - centerAzimuth);
isBetweenNorth := false;
// If values are on either side of the Azimuth of North we need to adjust it. Only check the degree range
if (centerAzimuth < ToDeg(FDegreeRange))
and (pointAzimuth > ToDeg(360 - FDegreeRange))
then
begin
result := centerAzimuth + (2 * PI - pointAzimuth);
isBetweenNorth := true;
end
else if (pointAzimuth < ToDeg(FDegreeRange))
and (centerAzimuth > ToDeg(360 - FDegreeRange))
then
begin
result := (pointAzimuth + (2 * PI - centerAzimuth));
isBetweenNorth := true;
end;
end;
function TARController.shouldDisplayCoordinate(
const coord: TARCoordinate): Boolean;
var
isBetweenNorth: Boolean;
deltaAzimuth: single;
begin
deltaAzimuth := findDeltaOfRadianCenter(FcenterCoordinate.azimuth, coord.azimuth, isBetweenNorth);
result := (deltaAzimuth <= ToDeg(FDegreeRange));
end;
function TARController.pointForCoordinate(
const coord: TARCoordinate): TARPoint;
var
isBetweenNorth: boolean;
deltaAzimith: single;
begin
isBetweenNorth := false;
deltaAzimith := findDeltaOfRadianCenter(FcenterCoordinate.azimuth, coord.azimuth, isBetweenNorth);
if (((coord.azimuth > FcenterCoordinate.azimuth)
and (
not isBetweenNorth))
or
((FcenterCoordinate.azimuth > ToDeg(360 - FDegreeRange))
and (coord.azimuth < ToDeg(FDegreeRange))))
then
result.x := (ARRectWidth(FDisplayView) / 2) + ((deltaAzimith / ToDeg(1)) * ADJUST_BY)
// Right side of Azimuth
else
result.x := (ARRectWidth(FDisplayView) / 2) - ((deltaAzimith / ToDeg(1)) * ADJUST_BY);
// Left side of Azimuth
result.y := (ARRectHeight(FDisplayView) / 2) + (toRad(PI / 2.0 + FViewAngle) * 2.0);
end;
procedure TARController.SetCenterLocation(
const ALocation: TARLocation);
var i: integer;
geoLocation: TARCoordinate;
begin
FcenterLocation := ALocation;
for i := 0
to FCoordinates.Count - 1
do
begin
geoLocation := FCoordinates[i];
if geoLocation
is TARGeoCoordinate
then
begin
(geoLocation
as TARGeoCoordinate).calibrateUsingOrigin(FCenterLocation);
if (geoLocation.radialDistance > FMaximumScaleDistance)
then
FMaximumScaleDistance := geoLocation.radialDistance;
end;
end;
end;
procedure TARController.UpdateAccelerometer(
const Ax, Ay, Az: single);
begin
case FCameraOrientation
of
coLandscapeLeft:
FViewAngle := arctan2(Ax, Az);
coLandscapeRight:
FViewAngle := arctan2(-Ax, Az);
coPortrait:
FViewAngle := arctan2(Ay, Az);
else
FViewAngle := arctan2(-Ay, Az);
end;
end;
procedure TARController.UpdateHeading(
const AHeading: single);
var adjustment: single;
begin
FLatestHeading := ToDeg(AHeading);
//Let's only update the Center Coordinate when we have adjusted by more than X degrees
if (abs(FLatestHeading - FPrevHeading) >= ToDeg(DEGREE_TO_UPDATE))
or
(FPrevHeading = HEADING_NOT_SET)
then
begin
FPrevHeading := FLatestHeading;
case FCameraOrientation
of
coLandscapeLeft: adjustment := ToDeg(270);
coLandscapeRight: adjustment := ToDeg(90);
coPortraitUpsideDown: adjustment := ToDeg(180);
else adjustment := 0;
end;
FCenterCoordinate.Azimuth := FLatestHeading - adjustment;
UpdateLocations();
end;
end;
procedure TARController.SetViewData(
const index: integer;
const AValue: Boolean);
begin
case index of
0: FScaleViewsBasedOnDistance := AValue;
1: FRotateViewsBasedOnPerspective := AValue;
end;
UpdateLocations;
end;
procedure TARController.SetScaleData(
const index: integer;
const AValue: Single);
begin
case index of
0: FMaximumScaleDistance := AValue;
1: FMinimumScaleFactor := AValue;
2: FMaximumRotationAngle := AValue;
end;
UpdateLocations;
end;
procedure TARController.SetDisplayView(
const ADisplayView: TARRect);
begin
FDisplayView := ADisplayView;
UpdateLocations;
end;
procedure TARController.UpdateLocations;
var i: integer;
item: TARGeoCoordinate;
loc: TARPoint;
scaleFactor: single;
transform: TARMatrix;
R: TARRect;
width, height: single;
begin
for i := 0
to FCoordinates.Count - 1
do
begin
item := FCoordinates[i];
if shouldDisplayCoordinate(item)
then
begin
loc := pointForCoordinate(item);
scaleFactor := SCALE_FACTOR;
if (FScaleViewsBasedOnDistance)
then
scaleFactor := scaleFactor - (FMinimumScaleFactor * item.radialDistance) / FMaximumScaleDistance;
width := item.width * scaleFactor;
height := item.height * scaleFactor;
R.Left := loc.x - width / 2.0;
R.Top := loc.y;
R.Right := R.Left + width;
R.Bottom := R.Top + height;
transform := ARIdentityMatrix;
// Set the scale if it needs it. Scale the perspective transform if we have one.
if FScaleViewsBasedOnDistance
then
transform := ARTransform3DScale(transform, scaleFactor, scaleFactor, scaleFactor);
if FRotateViewsBasedOnPerspective
then
transform.m34 := 1.0 / 300.0;
SetTransform(R, transform);
item.DisplayView := R;
end;
end;
end;
end.