views:

232

answers:

3

I have the following:

  • An image - a hand drawn map - of an area of roughly 600x400 meters. The image is drawn on top of Google Maps tiles.
  • The latitude/longitude (from Google Maps) of the corners of this image. Or put differently, I have the north and south latitude and the east and west longitude of the image.
  • A latitude/longitude coordinate from iPhone's CoreLocation.

How do I draw a point on this image (or nothing if it's out of bounds), representing the coordinate from CoreLocation?

Added bonus: draw an arrow on the edge of the map, pointing to the coordinate, if the coordinate is out of bounds of the image.

I would like to do this without using a library like proj, in order to not have to bundle a large library, and understand what I'm doing and why.

As you probably guessed by know, I'm writing this in Objective-C. Your answer doesn't have to be in Objective-C, though.

+1  A: 

Have you looked into using MapKit? It has methods for converting map coordinates to view coordinates. Have a look at the convert family of methods.

http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MKMapView_Class/MKMapView/MKMapView.html

If you are on 4.0 only, you might benefit from the overlay class as well.

http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MKOverlayView_class/Reference/Reference.html

Cheers!

logancautrell
+2  A: 

If I understand it correctly, you need to do two things. The first is to put your custom image into a map view and have your custom tiles appear at the correct coordinates, then pan, zoom and so on. The second thing you need to do is to draw a point onto that image at a certain latitude and longitude.

What you need is custom overlays, available in iOS 4 and up. The best place to find out about custom overlays is the WWDC 2010 video called "Session 127 - Customizing Maps with Overlays". There is also custom code available for the video. In the video, the presenter creates a custom map and embeds it in an MKMapView. He also describes a tool which you can use to make your tiles (to cut them up, get their shapes into the Mercator projection and name them properly). His map is scanned from a marine map, then placed on top of the normal map view.

You would be able to use boundingMapRect to create a bounds rectangle by converting your custom map's bounds to points. You can convert between points and coordinates using MKMapPointForCoordinate and MKCoordinateForMapPoint.

As for getting a point drawn on the map, you can do this a couple of ways. The easiest is to just use a custom MKAnnotationView with a dot as its image. This way the image doesn't grow and shrink as you zoom in. If you want the dot to grow and shrink, you should use a custom overlay for that too. You could easily use an MKCircleView, which is a subclass of MKOverlayView

For an arrow, you could use a normal view and rotate it (and place it on one side of the screen) according to the direction of your out-of-bounds point. Use MKMapPointForCoordinate and then calculate the directtion from the centre of your view.

But your best source is going to be that video. He goes into great depth about the whole process and gives source for a working app which is 90% of what you need for your own map.

nevan
Since I only have one image that is stored in the app bundle, it would be kind of silly to download map tiles from google in addition to my own image. How can I stop MapKit from downloading Google tiles? Can't find anything in the docs, and the WWDC video shows an opaque map on top of Google's tiles..
August Lilleaas
You'll actually end up with more than one image. You need to run a tool called GDAL (http://www.gdal.org/) to cut your big image into tiles at various zoom levels. Higher zooms will have more tiles. I'm not sure if you can stop MapKit from downloading tiles, but I'd assume that if your own map is 100% opaque, it wouldn't download them. In the video his map is actually slightly transparent. He talks about being able to see a ship on the satellite. Look at Google Maps near San Francisco and you'll be able to see the same ships.
nevan
A: 

After some research, I wrote my own library: libPirateMap. It's not very polished, but it works.

In case the link goes down, I'll paste the relevant source code here.

Usage:

// .h
PirateMap *pirateMap;
PirateMapPoint *pirateMapPoint;

// .m
- (void)viewDidLoad {
    [super viewDidLoad];
    pirateMap = [[PirateMap alloc] initWithNorthLatitude:59.87822
                                        andSouthLatitude:59.87428
                                        andWestLongitude:10.79847
                                        andEastLongitude:10.80375
                                           andImageWidth:640
                                          andImageHeight:960];

    pirateMapPoint = [[PirateMapPoint alloc] init];
    pirateMapPoint.pirateMap = pirateMap;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
    pirateMapPoint.coordinate = PirateMapCoordinate2DMake(newLocation.coordinate.latitude, newLocation.coordinate.longitude)    
    PirateMapPoint2D point = [pirateMapPoint pointOnImage];
    // use point.x and point.y to place your view.
}

Relevant .m-code.

#import "PirateMap.h"

static const double RAD_TO_DEG = 180 / M_PI;
static const double DEG_TO_RAD = M_PI / 180;

PirateMapCoordinate2D PirateMapCoordinate2DMake(double latitude, double longitude) {
    return (PirateMapCoordinate2D) {latitude, longitude};
}

// atan2(y2-y1,x2-x1)

@implementation PirateMap
@synthesize northLatitude, southLatitude, westLongitude, eastLongitude,
imageWidth, imageHeight, latitudeImageToWorldRatio, longitudeImageToWorldRatio;

-(id)initWithNorthLatitude:(double)aNorthLatitude
          andSouthLatitude:(double)aSouthLatitude
          andWestLongitude:(double)aWestLongitude
          andEastLongitude:(double)anEastLongitude
             andImageWidth:(int)anImageWidth
            andImageHeight:(int)anImageHeight{
    if (self = [super init]) {
        self.northLatitude = aNorthLatitude;
        self.southLatitude = aSouthLatitude;
        self.westLongitude = aWestLongitude;
        self.eastLongitude = anEastLongitude;

        self.imageWidth = anImageWidth;
        self.imageHeight = anImageHeight;

        self.latitudeImageToWorldRatio = [self computeLatitudeImageToWorldRatio];
        self.longitudeImageToWorldRatio = [self computeLongitudeImageToWorldRatio];
    }
    return self;
}

-(double)computeLatitudeImageToWorldRatio {
    return fabs(self.northLatitude - self.southLatitude) / self.imageHeight;
}

-(double)computeLongitudeImageToWorldRatio {
    return fabs(self.eastLongitude - self.westLongitude) / self.imageWidth;
}

+(double)latitudeToMercatorY:(double)latitude {
    static const double M_PI_TO_4 = M_PI / 4;

    return RAD_TO_DEG * log(tan(M_PI_TO_4 + latitude * (DEG_TO_RAD / 2)));
}
@end

#import "PirateMapPoint.h"

PirateMapPoint2D PirateMapPoint2DMake(int x, int y) {
    return (PirateMapPoint2D) {x, y};
}

@implementation PirateMapPoint
@synthesize pirateMap, coordinate;

-(id)initWithPirateMap:(PirateMap *)aPirateMap andCoordinate:(PirateMapCoordinate2D)aCoordinate {
    if (self = [super init]) {
        self.pirateMap = aPirateMap;
        self.coordinate = aCoordinate;
    }

    return self;
}

-(PirateMapPoint2D)pointOnImage {
    double xDelta = self.coordinate.longitude - self.pirateMap.westLongitude;
    double yDelta = self.pirateMap.northLatitude - self.coordinate.latitude;
    return PirateMapPoint2DMake(round(xDelta / self.pirateMap.longitudeImageToWorldRatio), round(yDelta / self.pirateMap.latitudeImageToWorldRatio));
}
@end
August Lilleaas