footprint/AAPLCoordinateConverter.m

/*
    Copyright (C) 2014 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this sample’s licensing information
    
    Abstract:
    
                This class converts coordinates from Geographic coordinate system to floorplan space.
            
*/
 
#import "AAPLCoordinateConverter.h"
@import MapKit;
 
// Struct that contains a point in meters (east and south) with respect to an origin point (in geographic space)
// We use East & South because when drawing on an image, origin (0,0) is on the top-left.
// So +eastMeters corresponds to +x and +southMeters corresponds to +y
typedef struct {
    CLLocationDistance east;
    CLLocationDistance south;
} AAPLEastSouthDistance;
 
@interface AAPLCoordinateConverter()
 
@property (assign, nonatomic) double radiansRotated;
 
// We pick one of the anchors on the floorplan as an origin point that we will compute distance relative to.
@property (assign, nonatomic) MKMapPoint fromAnchorMKPoint;
 
@property (assign, nonatomic) CGPoint fromAnchorFloorplanPoint;
 
@end
 
@implementation AAPLCoordinateConverter
 
// Convenience function to convert a MapKit co-ordinate into a co-ordinate meters East/South relative to some origin.
+ (AAPLEastSouthDistance)metersFromPoint:(const MKMapPoint)fromAnchorMKPoint toPoint:(const MKMapPoint)to {
    CLLocationDistance metersPerMapPoint = MKMetersPerMapPointAtLatitude(MKCoordinateForMapPoint(fromAnchorMKPoint).latitude);
 
    AAPLEastSouthDistance eastSouthDistance = {
        .east = (to.x - fromAnchorMKPoint.x) * metersPerMapPoint,
        .south = (to.y - fromAnchorMKPoint.y) * metersPerMapPoint,
    };
    return eastSouthDistance;
}
 
- (instancetype) initWithCoordinatesTopLeft:(CLLocationCoordinate2D)topLeft bottomRight:(CLLocationCoordinate2D)bottomRight imageSize:(CGSize)imageSize {
 
    AAPLGeoAnchor topLeftAnchor = {
        .latitudeLongitude = topLeft,
        .pixel = CGPointMake(0, 0)
    };
 
    AAPLGeoAnchor bottomRightAnchor = {
        .latitudeLongitude = bottomRight,
        .pixel = CGPointMake(imageSize.width, imageSize.height)
    };
 
    AAPLGeoAnchorPair anchorPair = {
        .fromAnchor = topLeftAnchor,
        .toAnchor = bottomRightAnchor
    };
 
    return [self initWithAnchors:anchorPair];
}
 
- (instancetype)initWithAnchors:(AAPLGeoAnchorPair)anchors {
    self = [super init];
    if (self) {
        // To compute the distance between two geographical co-ordinates, we first need to
        // convert to MapKit co-ordinates...
        _fromAnchorFloorplanPoint = anchors.fromAnchor.pixel;
        _fromAnchorMKPoint = MKMapPointForCoordinate(anchors.fromAnchor.latitudeLongitude);
        MKMapPoint toAnchorMapkitPoint = MKMapPointForCoordinate(anchors.toAnchor.latitudeLongitude);
 
        CGFloat xDistance = anchors.toAnchor.pixel.x - anchors.fromAnchor.pixel.x;
        CGFloat yDistance = anchors.toAnchor.pixel.y - anchors.fromAnchor.pixel.y;
 
        // ... so that we can use MapKit's helper function to compute distance.
        // this helper function takes into account the curvature of the earth.
        CLLocationDistance distanceBetweenPointsMeters = MKMetersBetweenMapPoints(_fromAnchorMKPoint, toAnchorMapkitPoint);
 
        // Distance between two points in pixels (on the floorplan image)
        CGFloat distanceBetweenPointsPixels = hypotf(xDistance, yDistance);
 
        // Get the 2nd anchor's eastward/southward distance in meters from the first anchor point.
        AAPLEastSouthDistance hyp = [AAPLCoordinateConverter metersFromPoint:_fromAnchorMKPoint toPoint:toAnchorMapkitPoint];
 
        // This gives us pixels/meter
        _pixelsPerMeter = distanceBetweenPointsPixels / distanceBetweenPointsMeters;
 
        // Angle of diagonal to east (in geographic)
        float angleFromEast = atan2(hyp.south, hyp.east);
 
        // Angle of diagonal horizontal (in floorplan)
        float angleFromHorizontal = atan2(yDistance, xDistance);
 
        // Rotation amount from the geographic anchor line segment
        // to the floorplan anchor line segment
        _radiansRotated = angleFromHorizontal - angleFromEast;
    }
 
    return self;
}
 
- (CGPoint)pointFromCoordinate:(CLLocationCoordinate2D)coordinate {
    // Get the distance east & south with respect to the first anchor point in meters
    AAPLEastSouthDistance toFix = [AAPLCoordinateConverter metersFromPoint:self.fromAnchorMKPoint toPoint:MKMapPointForCoordinate(coordinate)];
    
    // Convert the east-south anchor point distance to pixels (still in east-south)
    CGPoint pixelsXYInEastSouth = CGPointApplyAffineTransform(CGPointMake(toFix.east, toFix.south), CGAffineTransformMakeScale(self.pixelsPerMeter, self.pixelsPerMeter));
    
    // Rotate the east-south distance to be relative to floorplan horizontal
    // This gives us an xy distance in pixels from the anchor point.
    CGPoint xy = CGPointApplyAffineTransform(pixelsXYInEastSouth, CGAffineTransformMakeRotation(self.radiansRotated));
    
    // however, we need the pixels from the (0, 0) of the floorplan
    // so we adjust by the position of the anchor point in the floorplan
    xy.x += self.fromAnchorFloorplanPoint.x;
    xy.y += self.fromAnchorFloorplanPoint.y;
    
    return xy;
}
 
@end