ObjC/Footprint/AAPLCoordinateConverter.m
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
This class converts PDF coordinates of a floorplan to Geographic coordinates on Earth. NOTE: This class can also be used for any "right-handed" coordinate system (other than PDF) but should not be used as-is for "Raster image" coordinates (such as PNGs or JPEGs) because those require left-handed coordinate frames. |
There are other reasons we discourage the use of raster images as indoor floorplans. See the code comments inside AAPLFloorplanOverlay initWithFloorplanURL: for more info. |
*/ |
#import "AAPLCoordinateConverter.h" |
@import MapKit; |
/** |
@param a a vector. |
@param b a vector. |
@return the dot product of the two input vectors. |
*/ |
static CGFloat AAPLCGVectorDot(CGVector a, CGVector b) { |
return a.dx * b.dx + a.dy * b.dy; |
} |
/** |
@param v the initial vector. |
@param scale how much to scale (e.g. 1.0, 1.5, 0.2, etc). |
@return a copy of the input vector, rescaled by the amount given. |
*/ |
static CGVector AAPLCGVectorScaled(CGVector v, CGFloat scale) { |
return (CGVector) { |
.dx = v.dx * scale, |
.dy = v.dy * scale |
}; |
} |
/** |
@param v the initial vector. |
@param radians how many radians you want to rotate by. |
@return a copy of the input vector, after being rotated in the "positive |
radians" direction by the amount given. |
*/ |
static CGVector AAPLCGVectorRotatedRadians(CGVector v, CGFloat radians) { |
CGFloat cosRadians = cos(radians); |
CGFloat sinRadians = sin(radians); |
return (CGVector) { |
.dx = +cosRadians * v.dx -sinRadians * v.dy, |
.dy = +sinRadians * v.dx +cosRadians * v.dy, |
}; |
} |
/** |
@param a Point A. |
@param b Point B. |
@return The midpoint between A and B. |
*/ |
static MKMapPoint AAPLMKMapPointMidpoint(MKMapPoint a, MKMapPoint b) { |
return (MKMapPoint) { |
.x = (a.x + b.x) * 0.5, |
.y = (a.y + b.y) * 0.5 |
}; |
} |
/** |
@param a point A. |
@param b point B. |
@return the mean of the two CGPoint objects. |
*/ |
static CGPoint AAPLCGPointAverage(CGPoint a, CGPoint b) { |
return (CGPoint) { |
.x = (a.x + b.x) * 0.5, |
.y = (a.y + b.y) * 0.5 |
}; |
} |
/** |
@param a coordinate A. |
@param b coordinate B. |
@return The distance between the two coordinates in meters. |
*/ |
static CLLocationDistance AAPLCLDistanceBetweenLocationCoordinates2D(CLLocationCoordinate2D a, CLLocationCoordinate2D b) { |
CLLocation *locA = [[CLLocation alloc]initWithLatitude:a.latitude longitude:a.longitude]; |
CLLocation *locB = [[CLLocation alloc]initWithLatitude:b.latitude longitude:b.longitude]; |
return [locA distanceFromLocation:locB]; |
} |
/** |
Struct that contains a position in meters (east and south) with respect to |
an origin position (in geographic space). We use East & South because |
\c MKMapPoint's x value is positive in the Eastward direction and |
\c MKMapPoint's y value is positive in the Southward direction. |
@param east The distance eastward. |
@param south The distance southward. |
*/ |
typedef struct { |
CLLocationDistance east; |
CLLocationDistance south; |
} AAPLEastSouthDistance; |
@implementation AAPLCoordinateConverter |
- (instancetype)initWithAnchorPair:(AAPLGeoAnchorPair)anchors { |
self = [super init]; |
if (self) { |
_anchors = anchors; |
/* |
Next, to compute the direction between two geographical |
co-ordinates, we first need to convert to MapKit coordinates... |
*/ |
MKMapPoint fromAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.fromAnchor.latitudeLongitude); |
MKMapPoint toAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.toAnchor.latitudeLongitude); |
CGVector pdfDisplacement = { |
.dx = anchors.toAnchor.PDFPoint.x - anchors.fromAnchor.PDFPoint.x, |
.dy = anchors.toAnchor.PDFPoint.y - anchors.fromAnchor.PDFPoint.y |
}; |
// ... so that we can use MapKit's Mercator coordinate system where +x is always eastward and +y is always southward. |
// Imagine an arrow connecting fromAnchor to toAnchor... |
double anchorDisplacementMapkitX = (toAnchorMercatorCoordinate.x - fromAnchorMercatorCoordinate.x); |
double anchorDisplacementMapkitY = (toAnchorMercatorCoordinate.y - fromAnchorMercatorCoordinate.y); |
/* |
What is the angle of this arrow (geographically)? |
atan2 always returns: |
exactly 0.0 radians if the arrow is exactly in the +x direction |
("MapKit's +x" is due East). |
positive radians as the arrow is rotated toward and through the |
+y direction ("MapKit's +y" is due South). |
In the case of MapKit, this is radians clockwise from due East. |
*/ |
float radiansClockwiseOfDueEast = atan2(anchorDisplacementMapkitY, anchorDisplacementMapkitX); |
/* |
That means if we rotate cgDisplacement COUNTER-clockwise by this |
value, it will be facing due east. In the CG coordinate frame, |
positive radians is counter-clockwise because in a PDF +x is |
rightward and +y is upward. |
*/ |
CGVector cgDueEast = AAPLCGVectorRotatedRadians(pdfDisplacement, radiansClockwiseOfDueEast); |
// Now, get the distance (in meters) between the two anchors... |
CLLocationDistance distanceBetweenAnchorsMeters = AAPLCLDistanceBetweenLocationCoordinates2D(anchors.fromAnchor.latitudeLongitude, anchors.toAnchor.latitudeLongitude); |
// ...and rescale so that it's exactly one meter in length. |
_oneMeterEastward = AAPLCGVectorScaled(cgDueEast, 1.0 / distanceBetweenAnchorsMeters); |
/* |
Lastly, due south is PI/2 clockwise of due east. |
In the CG coordinate frame, clockwise rotation is NEGATIVE radians |
because in a PDF +x is rightward and +y is upward. |
*/ |
_oneMeterSouthward = AAPLCGVectorRotatedRadians(_oneMeterEastward, -M_PI_2); |
/* |
We'll choose the midpoint between the two anchors to be our "tangent |
point". This is the MKMapPoint that will correspond to both |
_tangentLatitudeLongitude on Earth and _tangentPDFPoint in the PDF. |
*/ |
MKMapPoint tangentMercatorCoordinate = AAPLMKMapPointMidpoint(fromAnchorMercatorCoordinate, toAnchorMercatorCoordinate); |
_tangentLatitudeLongitude = MKCoordinateForMapPoint(tangentMercatorCoordinate); |
_tangentPDFPoint = AAPLCGPointAverage(anchors.fromAnchor.PDFPoint, anchors.toAnchor.PDFPoint); |
} |
return self; |
} |
- (MKMapPoint)MKMapPointFromPDFPoint:(CGPoint)PDFPoint { |
/* |
To perform this conversion, we start by seeing how far we are from the |
tangentPoint. The tangentPoint is the one place on the PDF where we know |
exactly the corresponding Earth latitude & lontigude. |
*/ |
CGVector displacementFromTangentPoint = CGVectorMake(PDFPoint.x - self.tangentPDFPoint.x, PDFPoint.y - self.tangentPDFPoint.y); |
// Now, let's figure out how far East & South we are from this tangentPoint. |
CGFloat dotProductEast = AAPLCGVectorDot(displacementFromTangentPoint, _oneMeterEastward); |
CGFloat dotProductSouth = AAPLCGVectorDot(displacementFromTangentPoint, _oneMeterSouthward); |
AAPLEastSouthDistance eastSouthDistanceMeters = { |
// How many meters Eastward is cgPoint from tangentPoint? |
.east = dotProductEast / AAPLCGVectorDot(_oneMeterEastward, _oneMeterEastward), |
// How many meters Southward is cgPoint from tangentPoint? |
.south = dotProductSouth / AAPLCGVectorDot(_oneMeterSouthward, _oneMeterSouthward) |
}; |
CLLocationDistance metersPerMapPoint = MKMetersPerMapPointAtLatitude(self.tangentLatitudeLongitude.latitude); |
MKMapPoint tangentMercatorCoordinate = MKMapPointForCoordinate(self.tangentLatitudeLongitude); |
/* |
Each meter is about (1.0 / metersPerMapPoint) MKMapPoints, as long as we |
are nearby _tangentLatitudeLongitude. So just move this many meters East |
and South and we're done! |
*/ |
MKMapPoint result = { |
.x = tangentMercatorCoordinate.x + eastSouthDistanceMeters.east / metersPerMapPoint, |
.y = tangentMercatorCoordinate.y + eastSouthDistanceMeters.south / metersPerMapPoint, |
}; |
return result; |
} |
- (CGAffineTransform)PDFToMapKitAffineTransform { |
CLLocationDistance metersPerMapPoint = MKMetersPerMapPointAtLatitude(self.tangentLatitudeLongitude.latitude); |
MKMapPoint tangentMercatorCoordinate = MKMapPointForCoordinate(self.tangentLatitudeLongitude); |
/* |
CGAffineTransform operations easier to construct in reverse-order. |
Start with the last operation: |
*/ |
CGAffineTransform resultOfTangentMercatorCoordinate = CGAffineTransformMakeTranslation(tangentMercatorCoordinate.x, tangentMercatorCoordinate.y); |
/* |
Revise the CGAffineTransform to first scale by |
(1.0 / metersPerMapPoint), and then perform the above translation. |
*/ |
CGAffineTransform resultOfEastSouthDistanceMeters = CGAffineTransformScale(resultOfTangentMercatorCoordinate, 1.0 / metersPerMapPoint, 1.0 / metersPerMapPoint); |
/* |
Revise the AffineTransform to first scale by |
(1.0 / AAPLCGVectorDot(...)) before performing the transform so far. |
*/ |
CGAffineTransform resultOfDotProduct = |
CGAffineTransformScale( |
resultOfEastSouthDistanceMeters, |
1.0 / AAPLCGVectorDot(_oneMeterEastward, _oneMeterEastward), |
1.0 / AAPLCGVectorDot(_oneMeterSouthward, _oneMeterSouthward) |
); |
/* |
Revise the affine transform to first perform dot products against our |
reference vectors before performing the transform so far. |
*/ |
CGAffineTransform resultOfDisplacementFromTangentPoint = |
CGAffineTransformConcat( |
CGAffineTransformMake( |
_oneMeterEastward.dx, _oneMeterEastward.dy, |
_oneMeterSouthward.dx, _oneMeterSouthward.dy, |
0.0, 0.0 |
), |
resultOfDotProduct |
); |
/* |
Lastly, revise the CGAffineTransform to first perform the initial |
subtraction before performing the remaining operations. |
Each meter is about (1.0 / metersPerMapPoint) MKMapPoints, as |
long as we are nearby _tangentLatitudeLongitude. |
*/ |
return CGAffineTransformTranslate( |
resultOfDisplacementFromTangentPoint, |
- self.tangentPDFPoint.x, |
- self.tangentPDFPoint.y |
); |
} |
- (CLLocationDistance)unitSizeInMeters { |
return 1.0 / hypot(_oneMeterEastward.dx, _oneMeterEastward.dy); |
} |
- (MKPolygon *)polygonFromPDFRectCorners:(CGRect)pdfRect { |
MKMapPoint corners[4]; |
corners[0] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMaxX(pdfRect), CGRectGetMaxY(pdfRect))]; |
corners[1] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMinX(pdfRect), CGRectGetMaxY(pdfRect))]; |
corners[2] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMinX(pdfRect), CGRectGetMinY(pdfRect))]; |
corners[3] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMaxX(pdfRect), CGRectGetMinY(pdfRect))]; |
return [MKPolygon polygonWithPoints:corners count:4]; |
} |
- (MKMapRect)boundingMapRectIncludingRotations:(CGRect)rect { |
// Start with the nominal rendering box for this rect is. |
MKMapRect nominalRenderingRect = [self polygonFromPDFRectCorners:rect].boundingMapRect; |
/* |
In order to account for all rotations, any bounding map rect must have |
diameter equal to the longest distance inside the rectangle. |
*/ |
double boundsDiameter = hypot(nominalRenderingRect.size.width, nominalRenderingRect.size.height); |
CGPoint rectCenterPoints = { |
.x = CGRectGetMidX(rect), |
.y = CGRectGetMidY(rect) |
}; |
MKMapPoint boundsCenter = [self MKMapPointFromPDFPoint:rectCenterPoints]; |
/* |
Return a square MKMapRect centered at boundsCenterMercator with edge |
length diameterMercator. |
*/ |
return MKMapRectMake( |
boundsCenter.x - boundsDiameter / 2.0, |
boundsCenter.y - boundsDiameter / 2.0, |
boundsDiameter, |
boundsDiameter); |
} |
- (CLLocationDirection)uprightMKMapCameraHeading { |
/* |
To make the floorplan upright, we want to rotate the floorplan +x vector |
toward due east. |
*/ |
CGFloat resultRadians = atan2(_oneMeterEastward.dy, _oneMeterEastward.dx); |
CLLocationDirection result = resultRadians * 180.0 / M_PI; |
/* |
According to the CLLocationDirection documentation we must store a |
positive value if it is valid. |
*/ |
return (result < 0.0) ? (result + 360.0) : result; |
} |
@end |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-28