Swift/Footprint/FloorplanOverlay.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
This class describes a floorplan for an indoor venue. |
*/ |
import Foundation |
import MapKit |
/// This class describes a floorplan for an indoor venue. |
@objc class FloorplanOverlay: NSObject, MKOverlay { |
/** |
Same as boundingMapRect but slightly larger to fit on-screen under |
any MKMapCamera rotation. |
*/ |
var boundingMapRectIncludingRotations = MKMapRect() |
/** |
Cache the CGAffineTransform used to help draw the floorplan to the |
screen inside an MKMapView. |
*/ |
var transformerFromPDFToMk = CGAffineTransform() |
/// Current floor level |
var floorLevel = 0 |
/** |
Reference to the internal page data of the selected page of the PDF you |
are drawing. It is very likely that the PDF of your floorplan is a |
single page. |
*/ |
var pdfPage: CGPDFPage |
/** |
Same as boundingMapRect, but more precise. |
The AAPLMapRectRotated you'll get here fits snugly accounting for the |
rotation of the floorplan (relative to North) whereas the |
boundingMapRect must be "North-aligned" since it's an MKMapRect. |
If you're still not 100% sure, toggle the "debug switch" in the sample |
code and look at the overlays that are drawn. |
*/ |
var floorplanPDFBox: MKMapRectRotated |
/// The PDF document to be rendered. |
fileprivate var pdfDoc: CGPDFDocument |
/** |
The coordinate converter for converting between PDF coordinates (point) |
and MapKit coordinates (MKMapPoint). |
*/ |
fileprivate var coordinateConverter: CoordinateConverter |
/// For debugging, remember the PDF page box selected at initialization. |
fileprivate var pdfBoxRectangle = CGRect.null |
/// MKOverlay protocol return values. |
var boundingMapRect = MKMapRect() |
var coordinate = CLLocationCoordinate2D() |
/** |
In this example, our floorplan is described by four things. |
1. The URL of a PDF. This is the visual data for the floorplan. |
2. The PDF page box to draw. This tells us which section of the PDF |
we will actually draw. |
3. A pair of anchors. This tells us where the floorplan appears in |
the real world. |
4. A floor level. This tells us which floor our floorplan represents |
- parameter floorplanUrl: the path to a PDF containing the drawing of |
the floorplan. |
- parameter pdfBox: which section of the PDF do we draw? |
- parameter andAnchors: real-world anchors of this floorplan |
-- opposite corners. |
- parameter forFloorLevel: which floor is it on? |
*/ |
init(floorplanUrl: URL, withPDFBox pdfBox: CGPDFBox, andAnchors anchors: GeoAnchorPair, forFloorLevel level: NSInteger) { |
assert(floorplanUrl.absoluteString.hasSuffix("pdf"), "Sanity check: The URL should point to a PDF file") |
/* |
Using raster images (such as PNG or JPEG) would create a number of |
complications, such as: |
+ you need multiple sizes of each image, and each would need its |
own GeoAnchorPair (see "Icon and Image Sizes" for iOS on |
developer.apple.com for more). |
+ raster/bitmap images use a different coordinate system than PDFs |
do, so the code from CoordinateConverter could not be used |
out-of-the-box. Instead, you would need a separate |
implementation of CoordinateConverter that works for left-handed |
coordinate frames. PDFs use a right-handed coordinate frame. |
+ text and fine details of raster images may not render as clearly |
as vector images when zoomed in. PDF is primarily a vector image |
format. |
+ some raster image formats, such as JPEG, are designed for |
photographs and may suffer from loss of detail due to |
compression artifacts when being used for floorplans. |
*/ |
coordinateConverter = CoordinateConverter(anchors: anchors) |
transformerFromPDFToMk = coordinateConverter.transformerFromPDFToMk() |
floorLevel = level |
/* |
Read the PDF file from disk into memory. Remember to CFRelease it |
when we dealloc. |
(see "The Create Rule" on developer.apple.com for more) |
*/ |
pdfDoc = CGPDFDocument(floorplanUrl as CFURL)! |
/* |
In this example the floorplan PDF has only one page, so we pick |
"page 1" of the PDF. |
*/ |
pdfPage = pdfDoc.page(at: 1)! |
// Figure out which region of the PDF is to be drawn. |
pdfBoxRectangle = pdfPage.getBoxRect(pdfBox) |
/* |
There is no need to display this floorplan if your MapView camera is |
beyond the four corners of the PDF page box. Thus, our |
boundingMapRect is based on the PDF page box corners in the |
MKMapPoint coordinate frame. |
*/ |
let polygonFromPDFRectCorners = coordinateConverter.polygonFromPDFRectCorners(pdfBoxRectangle) |
boundingMapRect = polygonFromPDFRectCorners.boundingMapRect |
/* |
We need a quick way to check whether your screen is currently |
looking inside vs. outside the floorplan, in order to "clamp" your |
MKMapView. |
*/ |
assert(polygonFromPDFRectCorners.pointCount == 4) |
let points = polygonFromPDFRectCorners.points() |
floorplanPDFBox = MKMapRectRotatedMake(points[0], corner2: points[1], corner3: points[2], corner4: points[3]) |
/* |
For the purposes of clamping MKMapCamera zoom, we need a slightly |
padded MKMapRect that allows the entire floorplan can be visible |
regardless of camera rotation. Otherwise, depending on the |
MKMapCamera rotation, auto-zoom might prevent the user from zooming |
out far enough to see the entire floorplan and/or auto-scroll might |
prevent the user from seeing the edge of the floorplan. |
*/ |
boundingMapRectIncludingRotations = coordinateConverter.boundingMapRectIncludingRotations(pdfBoxRectangle) |
// For coordinate just return the centroid of boundingMapRect |
coordinate = MKCoordinateForMapPoint(boundingMapRect.getCenter()) |
} |
/** |
This is different from CoordinateConverter getUprightMKMapCameraHeading |
because here we also account for the PDF Page Dictionary's Rotate entry. |
- returns: the MKMapCamera heading required to display your *floorplan* |
upright. |
*/ |
func getFloorplanUprightMKMapCameraHeading() -> CLLocationDirection { |
/* |
Applying this heading to the MKMapCamera will cause PDF +x to face |
MapKit +x. |
*/ |
let rotatePDFXToMapKitX = coordinateConverter.getUprightMKMapCameraHeading() |
/* |
If a PDF Page Dictionary contains the "Rotate" entry, it is a |
request to the reader to rotate the _printed_ page *clockwise* by |
the given number of degrees before reading it. |
*/ |
let pdfPageDictionaryRotationEntryDegrees = pdfPage.rotationAngle |
/* |
In the MapView world that is equivalent to subtracting that amount |
from the MKMapCamera heading. |
*/ |
let result = CLLocationDirection(rotatePDFXToMapKitX) - CLLocationDirection(pdfPageDictionaryRotationEntryDegrees) |
/* |
According to the CLLocationDirection documentation we must store a |
positive value if it is valid. |
*/ |
return ((result < CLLocationDirection(0.0)) ? (result + CLLocationDirection(360.0)) : result) |
} |
/** |
Create an MKPolygon overlay given a custom CGPath (whose coordinates |
are specified in the PDF points) |
- parameter pdfPath: an array of CGPoint, each element is a PDF |
coordinate along the path. |
- returns: A closed MapKit polygon made up of the points in PDF path. |
*/ |
func polygonFromCustomPDFPath(_ pdfPath: [CGPoint]) -> MKPolygon { |
// Calculate the corresponding MKMapPoint for each PDF point. |
var coordinates = pdfPath.map { pathPoint in |
return coordinateConverter.MKMapPointFromPDFPoint(pathPoint) |
} |
return MKPolygon(points: &coordinates, count: coordinates.count) |
} |
/** |
For debugging, you may want to draw the reference anchors that define |
this floor's coordinate converter. |
*/ |
var geoAnchorPair: GeoAnchorPair { |
return coordinateConverter.anchors |
} |
/// For debugging, you may want to draw the the (0.0, 0.0) point of the PDF. |
var pdfOrigin: MKMapPoint { |
return coordinateConverter.MKMapPointFromPDFPoint(CGPoint.zero) |
} |
/** |
For debugging, you may want to know the real-world coordinates of the |
PDF page box. |
*/ |
var polygonFromFloorplanPDFBoxCorners: MKPolygon { |
return coordinateConverter.polygonFromPDFRectCorners(pdfBoxRectangle) |
} |
/** |
For debugging, you may want to have the boundingMapRect in the form of |
an MKPolygon overlay |
*/ |
var polygonFromBoundingMapRect: MKPolygon { |
return boundingMapRect.polygonFromMapRect() |
} |
/** |
For debugging, you may want to have the |
boundingMapRectIncludingRotations in the form of an MKPolygon overlay |
*/ |
var polygonFromBoundingMapRectIncludingRotations: MKPolygon { |
return boundingMapRectIncludingRotations.polygonFromMapRect() |
} |
/** |
For debugging, you may want to know the real-world meters size of one |
PDF "point" distance. |
*/ |
var pdfPointSizeInMeters: CLLocationDistance { |
return coordinateConverter.unitSizeInMeters |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-28