Implementing an AR Hotel Finder with ARKit: Coordinate Systems, Transformations, and Navigation
This article details the design and implementation of an ARKit‑based hotel‑finding application for iOS, covering coordinate system concepts, conversion pipelines, elevation handling, directional hints, radar visualization, navigation routing, map/AR switching, and real‑time navigation alerts, accompanied by sample Objective‑C code.
Introduction
Since WWDC 2017 Apple released ARKit, enabling developers to add powerful augmented‑reality features without third‑party frameworks. This article describes a prototype that helps users locate nearby hotels in a real‑world scene using ARKit.
Coordinate Systems
The project works with five coordinate systems:
Object coordinate system : local coordinates describing an object's size and position.
World coordinate system : the absolute reference for all points before a user‑defined coordinate system is established.
Camera (view) coordinate system : a right‑handed system whose origin is the intersection of the optical axis and the image plane.
Projection coordinate system : a 2‑D system obtained by projecting 3‑D points onto a screen‑like plane.
Screen coordinate system : the 2‑D pixel coordinates of the device display.
The transformation order is Object → World → Camera → Projection → Screen, with attention to left‑hand/right‑hand differences and unit conversions.
// Related conversion code
- (LocationTranslation)translationToLocation:(CLLocation *)location
{
CLLocation *inbetweenLocation = [[CLLocation alloc] initWithLatitude:self.coordinate.latitude longitude:location.coordinate.longitude];
CLLocationDistance distanceLatitude = [location distanceFromLocation:inbetweenLocation];
double latitudeTranslation;
if (location.coordinate.latitude > inbetweenLocation.coordinate.latitude) {
latitudeTranslation = distanceLatitude;
} else {
latitudeTranslation = 0 - distanceLatitude;
}
CLLocationDistance distanceLongitude = [self distanceFromLocation:inbetweenLocation];
double longitudeTranslation;
if (self.coordinate.longitude > inbetweenLocation.coordinate.longitude) {
longitudeTranslation = 0 - distanceLongitude;
} else {
longitudeTranslation = distanceLongitude;
}
CLLocationDistance altitudeTranslation = location.altitude - self.altitude;
return [TCTARCLUtil getLocationWith:latitudeTranslation longitudeTranslation:longitudeTranslation altitudeTranslation:altitudeTranslation];
}
+ (LocationTranslation)getLocationWith:(double)latitudeTranslation longitudeTranslation:(double)longitudeTranslation altitudeTranslation:(double)altitudeTranslation
{
LocationTranslation location = {latitudeTranslation,longitudeTranslation,altitudeTranslation};
return location;
}Elevation of Hotels
To avoid overlapping hotel labels, the app checks whether two hotels intersect on screen using projectPoint to obtain 2‑D coordinates, builds CGRect for each label, and applies CGRectIntersectsRect . Overlapping hotels are grouped and then raised sequentially based on distance.
NSMutableArray *intersectsArray = [NSMutableArray new];
for (TCTSCNVector3Object *object in _originPositions) {
SCNVector3 thisPoint = [self projectPoint:object.position];
CGRect thisRect = CGRectMake(thisPoint.x, thisPoint.y, 150, 40);
for (TCTSCNVector3Object *nextObject in _originPositions) {
if (![object isEqual:nextObject]) {
SCNVector3 nextPoint = [self projectPoint:nextObject.position];
CGRect nextRect = CGRectMake(nextPoint.x, nextPoint.y, 150, 40);
if (CGRectIntersectsRect(thisRect, nextRect)) {
[intersectsArray addObject:@[@(object.index), @(nextObject.index)]];
}
}
}
}Hotel Direction Hint
When the user’s device orientation does not include a hotel node, the app uses isNodeInsideFrustum to detect visibility, then calculates distances from the hotel to the left and right edges of the screen to decide whether to show a left or right directional tip.
SCNNode *pointOfView = renderer.pointOfView;
BOOL isVisible = [renderer isNodeInsideFrustum:_currentNode withPointOfView:pointOfView];
SCNVector3 thisPoint = [renderer projectPoint:_currentNode.position];
CGPoint leftPoint = CGPointMake(0, thisPoint.y);
CGPoint rightPoint = CGPointMake(SCREEN_WIDTH, thisPoint.y);
SCNVector3 leftWorldPosition = [renderer unprojectPoint:SCNVector3Make(leftPoint.x, leftPoint.y, 0)];
SCNVector3 rightWorldPosition = [renderer unprojectPoint:SCNVector3Make(rightPoint.x, rightPoint.y, 0)];
CGFloat leftDistance = [TCTARCLUtil distanceToAnotherVector:_currentNode.position anotherVector:leftWorldPosition];
CGFloat rightDistance = [TCTARCLUtil distanceToAnotherVector:_currentNode.position anotherVector:rightWorldPosition];
dispatch_async(dispatch_get_main_queue(), ^{
if (isVisible) {
[_leftRightTipView turnToState:HTARLeftRightTipViewStateHidden positionValue:0];
} else {
if (leftDistance > rightDistance) {
[_leftRightTipView turnToState:HTARLeftRightTipViewStateRight positionValue:thisPoint.y];
} else {
[_leftRightTipView turnToState:HTARLeftRightTipViewStateLeft positionValue:thisPoint.y];
}
}
});Radar Implementation
The radar shows the direction of hotels when none are visible. The user’s GPS location is the radar’s center; the radius equals the distance to the farthest hotel. Heading updates from CLLocationManager rotate the radar needle using CATransform3DRotate .
Navigation Implementation
Walking navigation is built with Apple’s MKDirections . The route’s polyline is sampled every 5 meters, and an arrow node is placed at each sample point in the AR scene.
// Create start and end placemarks
MKPlacemark *fromPlace = [[MKPlacemark alloc] initWithCoordinate:userLocation addressDictionary:nil];
MKPlacemark *toPlace = [[MKPlacemark alloc] initWithCoordinate:hotelLocation addressDictionary:nil];
MKMapItem *fromItem = [[MKMapItem alloc] initWithPlacemark:fromPlace];
MKMapItem *toItem = [[MKMapItem alloc] initWithPlacemark:toPlace];
MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init];
request.source = fromItem;
request.destination = toItem;
request.transportType = MKDirectionsTransportTypeWalking;
MKDirections *directions = [[MKDirections alloc] initWithRequest:request];
[directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {}]; // Draw arrows along the route
CLLocationDistance distance = [_startLocation distanceFromLocation:_endNode.location];
NSUInteger count = distance / 5; // one arrow per 5 m
_lastVector = SCNVector3Make(0, 0, 0);
for (int i = 1; i < count; i++) {
SCNVector3 vector = {(_endNode.position.x - _startVector.x) / count * i + _startVector.x,
-2,
(_endNode.position.z - _startVector.z) / count * i + _startVector.z};
SCNNode *node = [self getArrowNodeWithStartVector:vector];
_lastVector = vector;
[self addChildNode:node];
}Automatic Switch Between Map and AR
The app monitors device orientation using CMMotionManager . When the phone is laid flat, it switches to a map view; when held upright, it returns to the AR view.
Real‑Time Navigation Reminder
Geofencing is implemented with AMap’s AMapGeoFenceManager . Small circular fences (15 m radius) are placed at each route waypoint; entering a fence triggers a callback that updates the navigation instructions.
self.fenceManager = [[AMapGeoFenceManager alloc] init];
self.fenceManager.delegate = self;
self.fenceManager.activeAction = AMapGeoFenceActiveActionInside | AMapGeoFenceActiveActionExit | AMapGeoFenceActiveActionStay;
[weakSelf.fenceManager addCircleRegionForMonitoringWithCenter:location.coordinate radius:15 customID:[NSString stringWithFormat:"%d", i]];The delegate method - (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager didGeoFencesStatusChangedForRegion:(AMapGeoFenceRegion *)region customID:(NSString *)customID error:(NSError *)error is used to refresh the UI when the user reaches a waypoint.
Conclusion and Outlook
ARKit provides a robust foundation for AR experiences, though power consumption remains a concern. Future work includes optimizing energy usage, extending the solution to Android via ARCore, and exploring additional scenarios for AR deployment.
Tongcheng Travel Technology Center
Pursue excellence, start again with Tongcheng! More technical insights to help you along your journey and make development enjoyable.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.