I have a MapView with <5 annotations. When the map loads, I want to zoom in to center the map on these annotations, like this. However, the linked method doesn't work when the annotations are spread across the 180th meridian (where longitude wraps from -180 to +180). How can I robustly zoom to the annotations even if they are clustered around the prime meridian?
For example, if I have x(0,179) and y(0,-179) then I'd want a containing rect with a width of two degrees longitude, not 358.
MapKit for iOS actually has a built-in function in MKMapView which does this:
showAnnotations(annotations: [MKAnnotation], animated: Bool)
However, this function doesn't offer control over the zoom level, so I still had to make my own function. Here is the pseudocode:
zoomToAnnotations(annotations, zoomLimit) {
// calculate the midpoint as the average latitude and longitude
average_latitude = mean(a.coordinate.latitude for a in annotations)
average_longitude = atan2(mean(sin(a.coordinate.longitude) for a in annotations), mean(cos(a.coordinate.longitude) for a in annotations)) // be careful with degrees and radians in real code
regular_midpoint = coordinate(average_latitude, average_longitude)
// subtract 180 from longitude for the midpoint of the region crossing the meridian
meridian_midpoint = coordinate(average_latitude, average_longitude - 180.0)
if (meridian_midpoint.longitude < -180) {
meridian_midpoint.longitude += 360.0
}
// work out which region will be smaller and center map there
meridian_sum = sum(a.coordinate.distance(meridian_midpoint) for a in annotations)
regular_sum = sum(a.coordinate.distance(regular_midpoint) for a in annotations)
regular_max_distance = max(a.coordinate.distance(regular_midpoint) for a in annotations)
if meridian_sum < regular_sum {
meridian_max_distance = max(a.coordinate.distance(meridian_midpoint) for a in annotations)
centerMapWithRadius(meridian_midpoint, max(zoomLimit, meridian_max_distance))
} else {
regular_max_distance = max(a.coordinate.distance(regular_midpoint) for a in annotations)
centerMapWithRadius(regular_midpoint, max(zoomLimit, regular_max_distance))
}
}
This method isn't perfect , as it centers on the mean of the points instead of having the furthest points equal distances from the edge.
Related
I would like to simplify a GPS coordinate to a "TOP LEFT" position of a defined virtual grid (for example 100 kilometers).
If my GPS coordinate is in a cell of the grid, then we use the GPS position of the "TOP LEFT" of the cell.
The new coordinate is not intended to be displayed on a map but just to be communicated and manipulated.
This imaginary grid would have an editable distance (e.g. 1 kilometer or 100 kilometers).
I had imagined calculating the distance between two known points:
latitude = 0 / longitude = 0 (Null Island)
has GPS coordinate (ex: lat 48.858284648396626 lng 2.294501066207886)
we calculate distances for lat/lng (i use leaflet distanceTo function )
distance_latitude: 5432.79 km (?) : distanceTo( [0,0],[48.858284648396626,0])
distance_longitude: 255.14 km (?) : distanceTo( [0,0],[0,2.294501066207886])
var distance=100; // in kilometers
distance_latitude= Math.floor(distance_latitude/ distance) * distance;
= 5400
distance_longitude= Math.floor(distance_longitude / distance) * distance;
= 200
but after that... how to transform these kilometers (from Null Island) to a new coordinate? (so the top left of the current cell where the poi is)
(grid not in scale!)
Sometimes it's better to wait for a good night's sleep!
answer: just move nullIsland the distance between the poi and... nullIsland... simply! So all the dots (red) take the TopLeft position -blue marker- of the imaginary grid (in my screenshot: a 5Km grid).
I need to create a MKCoordinateSpan that is about 500 meters.
How do I calculate the values to pass into the MKCoordinateSpan constructor?
Answers in any programming (Obj-C, .Net) language are fine.
Another alternative is to use MapKit's MKCoordinateRegionMakeWithDistance function:
MKCoordinateRegion rgn = MKCoordinateRegionMakeWithDistance(
CLLocationCoordinate2DMake(someLatitude, someLongitude), 500, 500);
The MKCoordinateSpan will be in rgn.span.
Unless you need great accuracy you can make it much easier with approximation. The first problem is to find the fraction of a degree of latitude representing 500 meters. Easy since a degree of latitude is a constant in any location, roughly 111 km. So 500 meters is .0045 degrees latitude.
Then it gets harder because length of a degree of longitude varies depending on where you are. It can be approximated with
where alpha is earth's equatorial radius, 6,378,137km, b/a is 0.99664719 (a constant in use for the WGC84 spheroid model in use by all GPS devices) and where phi is the degree of latitude.
Imagine for a second you're lucky enough to be in Melbourne with a longitude of 37.783 degrees S. North or South doesn't matter here. beta works out to be 37.6899 and the rest of it solves to give a longitudinal degree a length of 88km. So 500 meters is .0057 of a degree.
Result for Melbourne - MKCoordinateSpan melbourne500MeterSpan = MKCoordinateSpanMake(.0045, .0057);
You can check your answers and your code with this online calculator
The wiki article on longitude has a lot more detail on this (and it the source of the images here)
Code:
#define EARTH_EQUATORIAL_RADIUS (6378137.0)
#define WGS84_CONSTANT (0.99664719)
#define degreesToRadians(x) (M_PI * (x) / 180.0)
// accepts decimal degrees. Convert from HMS first if that's what you have
double spanOfMetersAtDegreeLongitude(double degrees, double meters) {
double tanDegrees = tanf(degreesToRadians(degrees));
double beta = tanDegrees * WGS84_CONSTANT;
double lengthOfDegree = cos(atan(beta)) * EARTH_EQUATORIAL_RADIUS * M_PI / 180.0;
double measuresInDegreeLength = lengthOfDegree / meters;
return 1.0 / measuresInDegreeLength;
}
In MonoTouch, then using this solution you can use this helper method:
public static void ZoomToCoordinateAndCenter (MKMapView mapView, CLLocationCoordinate2D coordinate, double meters, bool showUserLocationToo, bool animate)
{
if (!coordinate.IsValid ())
return;
mapView.SetCenterCoordinate (coordinate, animate);
mapView.SetRegion (MKCoordinateRegion.FromDistance (coordinate, meters, meters), animate);
}
I must be missing somthing out in the docs, I thought this should be easy...
If I have one coordinate and want to get a new coordinate x meters away, in some direction. How do I do this?
I am looking for something like
-(CLLocationCoordinate2D) translateCoordinate:(CLLocationCoordinate2D)coordinate
translateMeters:(int)meters
translateDegrees:(double)degrees;
Thanks!
Unfortunately, there's no such function provided in the API, so you'll have to write your own.
This site gives several calculations involving latitude/longitude and sample JavaScript code. Specifically, the section titled "Destination point given distance and bearing from start point" shows how to calculate what you're asking.
The JavaScript code is at the bottom of that page and here's one possible way to convert it to Objective-C:
- (double)radiansFromDegrees:(double)degrees
{
return degrees * (M_PI/180.0);
}
- (double)degreesFromRadians:(double)radians
{
return radians * (180.0/M_PI);
}
- (CLLocationCoordinate2D)coordinateFromCoord:
(CLLocationCoordinate2D)fromCoord
atDistanceKm:(double)distanceKm
atBearingDegrees:(double)bearingDegrees
{
double distanceRadians = distanceKm / 6371.0;
//6,371 = Earth's radius in km
double bearingRadians = [self radiansFromDegrees:bearingDegrees];
double fromLatRadians = [self radiansFromDegrees:fromCoord.latitude];
double fromLonRadians = [self radiansFromDegrees:fromCoord.longitude];
double toLatRadians = asin( sin(fromLatRadians) * cos(distanceRadians)
+ cos(fromLatRadians) * sin(distanceRadians) * cos(bearingRadians) );
double toLonRadians = fromLonRadians + atan2(sin(bearingRadians)
* sin(distanceRadians) * cos(fromLatRadians), cos(distanceRadians)
- sin(fromLatRadians) * sin(toLatRadians));
// adjust toLonRadians to be in the range -180 to +180...
toLonRadians = fmod((toLonRadians + 3*M_PI), (2*M_PI)) - M_PI;
CLLocationCoordinate2D result;
result.latitude = [self degreesFromRadians:toLatRadians];
result.longitude = [self degreesFromRadians:toLonRadians];
return result;
}
In the JS code, it contains this link which shows a more accurate calculation for distances greater than 1/4 of the Earth's circumference.
Also note the above code accepts distance in km so be sure to divide meters by 1000.0 before passing.
I found one way of doing it, had to dig to find the correct structs and functions. I ended up not using degrees but meters for lat and long instead.
Here's how I did it:
-(CLLocationCoordinate2D)translateCoord:(CLLocationCoordinate2D)coord MetersLat:(double)metersLat MetersLong:(double)metersLong{
CLLocationCoordinate2D tempCoord;
MKCoordinateRegion tempRegion = MKCoordinateRegionMakeWithDistance(coord, metersLat, metersLong);
MKCoordinateSpan tempSpan = tempRegion.span;
tempCoord.latitude = coord.latitude + tempSpan.latitudeDelta;
tempCoord.longitude = coord.longitude + tempSpan.longitudeDelta;
return tempCoord;
}
And of course, if I really need to use degrees in the future, it's pretty easy (I think...) to do some changes to above to get it to work like I actually asked for.
Using an MKCoordinateRegion has some issues—the returned region can be adjusted to fit since the two deltas may not exactly map to the projection at that latitude, if you want zero delta for one of the axes you are out of luck, etc.
This function uses MKMapPoint to perform coordinate translations which allows you to move points around in the map projection's coordinate space and then extract a coordinate from that.
CLLocationCoordinate2D MKCoordinateOffsetFromCoordinate(CLLocationCoordinate2D coordinate, CLLocationDistance offsetLatMeters, CLLocationDistance offsetLongMeters) {
MKMapPoint offsetPoint = MKMapPointForCoordinate(coordinate);
CLLocationDistance metersPerPoint = MKMetersPerMapPointAtLatitude(coordinate.latitude);
double latPoints = offsetLatMeters / metersPerPoint;
offsetPoint.y += latPoints;
double longPoints = offsetLongMeters / metersPerPoint;
offsetPoint.x += longPoints;
CLLocationCoordinate2D offsetCoordinate = MKCoordinateForMapPoint(offsetPoint);
return offsetCoordinate;
}
Nicsoft's answer is fantastic and exactly what I needed. I've created a Swift 3-y version which is a little more concise and can be called directly on a CLLocationCoordinate2D instance:
public extension CLLocationCoordinate2D {
public func transform(using latitudinalMeters: CLLocationDistance, longitudinalMeters: CLLocationDistance) -> CLLocationCoordinate2D {
let region = MKCoordinateRegionMakeWithDistance(self, latitudinalMeters, longitudinalMeters)
return CLLocationCoordinate2D(latitude: latitude + region.span.latitudeDelta, longitude: longitude + region.span.longitudeDelta)
}
}
I'm attempting to draw a circle around a point on a mapview, which I have successfully done, but not quite the way I wanted to. The CG methods are always going to be drawing relative to the screen size, and basically I want to draw things in meters, not pixels.
Anyone have experience doing this?
I don't have any experience doing what you describe above, but the MKMapView class has a set of methods for concerting Pixels to Coordinates and vice versa that you should be able to use to map your circle to Coordinates on the Map:
http://developer.apple.com/iphone/library/documentation/MapKit/Reference/MKMapView_Class/MKMapView/MKMapView.html#//apple_ref/occ/instm/MKMapView/convertCoordinate:toPointToView:
This function may also come in handy for finding a point on the diamater of your circle to use with the above functions, providing you have your center point, the radius in meter of the circle, and the bearing in degrees.
-(CLLocation*) offsetLocation:(CLLocation*)startLocation:(double)offsetMeters:(double)bearing
{
double EARTH_MEAN_RADIUS_METERS = 6372796.99;
double lat2 = asin( sin(startLocation.coordinate.latitude) * cos(offsetMeters/EARTH_MEAN_RADIUS_METERS) + cos(startLocation.coordinate.latitude) * sin(offsetMeters/EARTH_MEAN_RADIUS_METERS) * cos(bearing) );
double lon2 = startLocation.coordinate.longitude + atan2( sin(bearing) * sin(offsetMeters/EARTH_MEAN_RADIUS_METERS) * cos(startLocation.coordinate.latitude), cos(offsetMeters/EARTH_MEAN_RADIUS_METERS) - sin(startLocation.coordinate.latitude) * sin(lat2));
CLLocation *tempLocation = [[CLLocation alloc] initWithLatitude:lat2 longitude:lon2];
return tempLocation;
}
When I debug code below, I see that span is changed by mapkit from what I have provided. span2 contains different numbers from what was provided. Why is this happening?
- (void) viewDidLoad
{
[super viewDidLoad];
CLLocationCoordinate2D loc;
loc.latitude = self.atm.lat;
loc.longitude = self.atm.lon;
MKCoordinateSpan span1 = MKCoordinateSpanMake(0.05f, 0.05f);
self.mapView.region = MKCoordinateRegionMake(loc, span1);
// at this point numbers are not 0.05 anymore
MKCoordinateSpan span2 = self.mapView.region.span;
// ... more code
}
Note that latitude and longitude change differently so square MapView will actually display region with different span values for its coordinates. This is likely to cause your mapview region to have span (slightly?) different from what you set.
In reference for MKMapView's region property there's somewhat relevant phrase:
Changing only the center coordinate of the region can still
cause the span to change implicitly.
This is due to the fact that the
distances represented by a span change
at different latitudes and longitudes
and the map view may need to adjust
the span to account for the new
location.
A square MapView with identical values for the latitudinal and longitudinal span will almost always experience the change in span indicated above as longitudinal arc-length varies significantly as you move from the equator to either pole. As the latitude approaches +/-90, the longitudinal arc-length approaches 0.
In addition, however, identical span values will also change if the MapView region is not square (as the span only refers to the region actually visible at the time).