Calculate bearing in MKMapView gives wrong value while crossing 180 meridian - ios

I need to draw lines to demonstrate transportation of goods on apple maps. To clarify start- and end-point, I draw a little arrowhead on the destination side.The arrowhead is drawn separately but it is reversed in one case.
>-->-->-->-
instead of
<--<--<--<-
I am using MKMapView and MKPolyline to draw lines. I am using MKOverlay to add direction arrows. The steps I follow are,
calculate bearing of
Source : CLLocationCoordinate2D(latitude: -33.8392932, longitude: 151.21519799999999)
Destination: CLLocationCoordinate2D(latitude: 39.645516999999998, longitude: -104.598724)
using the following function
open static func getDirectionOf( _ supplyLineWithCoordinates: [CLLocationCoordinate2D]) -> CGFloat {
guard let sourceCoordniate = supplyLineWithCoordinates.first,
let destinationCoordniate = supplyLineWithCoordinates.last else {
fatalError("Coordinates of supply line not found")
}
let sourcePoint: MKMapPoint = MKMapPointForCoordinate(sourceCoordniate)
let destinationPoint: MKMapPoint = MKMapPointForCoordinate(destinationCoordniate)
let x: Double = destinationPoint.x - sourcePoint.x
let y: Double = destinationPoint.y - sourcePoint.y
var arrowDirection = CGFloat(fmod(atan2(y, x), 360.0))
if arrowDirection < 0.0 {
arrowDirection += 2 * .pi
}
return arrowDirection
}
Rotate the arrow image and add it as the map overlay. The directions are calculated correctly in most of the cases, however, when I select the line shown below the direction is displayed 180 opposite. It starts from Sydney, Australia and ends in Denver, US
When trying to display the region with this two locations in mapView.setVisibleMapRect these region is not displayed, mapview tries to display region starting from Sydney (Australia) to Denver(US) through Asia and Europe, while it should display the map area I have attached above. If you have suggestions for optimisation, feel free to mention it.
I think this might be the reason, the direction should be calculated along the red line but it being calculated along the green line. Both lines are drawn by connecting same location coordinates in map. Any known workaround for this?

I solved it in a dirty way by converting coordinate to CGPoint and then calculating bearing between Points.
let destinationPoint = mapView.convert(destination, toPointTo: nil)
let sourcePoint = mapView.convert(source, toPointTo: nil)
let bearing = atan2(sourcePoint.y - destinationPoint.y, sourcePoint.x - destinationPoint.x) - .pi
Caution: This calculation will go wrong when map is rotated

Related

Calculate coordinates of map square from map center

I want to get the coordinates of the corners of the rectangle. Or to find the coordinate of the north-westest most point, 50 km from the map centre.
Does anyone know how I can do that?
The point is when I move around the map, I want to always have a rectangle(the rectangle does not need to drew, I just need its coordinates for a backend request), with it's corners always at 50 km from the current centre of the map.
I'm thinking of using somehow the distance function from CLLocation, but in this case I have the distance, but not one of the coordinates.
50km = mapCenterLocation.distance(from: coordinatesUnknown)
Not really sure what do you mean, but maybe this can help
func getNewTargetCoordinate(position: CLLocationCoordinate2D, userBearing: Float, distance: Float)-> CLLocationCoordinate2D{
//haversine formula
//r is earth radius
let r = 6378140.0
let latitude1 = position.latitude * (Double.pi/180);
let longitude1 = position.longitude * (Double.pi/180);
//bearing for user heading in degree
let brng = Double(userBearing) * (Double.pi/180);
//calculating user new position based on user distance and bearing can be seen at haversine formula
var latitude2 = asin(sin(latitude1)*cos(Double(distance)/r) + cos(latitude1)*sin(Double(distance)/r)*cos(brng));
var longitude2 = longitude1 + atan2(sin(brng)*sin(Double(distance)/r)*cos(latitude1),cos(Double(distance)/r)-sin(latitude1)*sin(latitude2));
//converting latitude as degree
latitude2 = latitude2 * (180/Double.pi)
longitude2 = longitude2 * (180/Double.pi)
// return location of user
return CLLocationCoordinate2DMake(latitude2, longitude2)
}
This work for NE direction and distance in meters
for the north-west direction, I think you can just put 135 for the degree and 5000 for distance.
For the position, you need to put map center location.
edit:
For custom rectangle., you can first check for the diagonal degree
func getDiagonalDegree(x: Float, y:Float) -> Float{
return atan2(y,x)*(180/Double.pi)
}
So now you can get that returned diagonal degree to and put it in getNewTargetCoordinate. New bearing is 270+diagonalDegree.
Not sure if I understand you correctly, but I think this could help, or at least point you on some direction
CLLocationCoordinate2D northWest;
northWest = [mapView convertPoint:CGPointMake(0, 0) toCoordinateFromView:mapView];
With this you will get the coordinates for the top left corner of the map, I think you just need to adjust this to set a point 50 km of your center and get the coordinate with this same logic.

Swift mkmapview get zoom level, width or radius of visible map area from latitude longitude delta

I am in a confusion, on how to get a zoom level and radius of a visible area of the map (using mapkit mapview).
Here is what I am looking for (either of them or both, as needed) -
Zoom level, is to see at what level of the map is being shown to the users and with that information, I want to display the custom pins in the visible area. If zoom level is high, I need to show the actual logos of some commerce stores as pins. If zoom level is low, I need to show colored dots instead of logos.
As of now, I am using let updatedRadius = (mapView.camera.altitude)/1000 to get altitude of the camera, and if the updatedRadius value is > 25.0, I am showing colored dots. Below 25.0, I show the logos. I am doing this in regionDidChanged
Is this approach correct?
Radius, is to send it as a parameter to my REST API to fetch the list of places within that radius. When user zooms out on the map, visible area increases and so the REST API needs bigger radius to return the places covered in that area.
Ultimately, what should happen is, whenever user zooms out, then the radius changes. I need to send this changed radius to my REST to get an updated list.
What are latitude longtitude deltas, can we get radius/width of visible area using these values?
let latitudeDeltaVal = mapView.region.span.latitudeDelta
let longitudeDeltaVal = mapView.region.span.longitudeDelta
Can someone throw some light on what needs to be done please?
Since you need to call the api when the region changes you need to calculate the radius in mapView's delegate function, RegionDidChange.
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let centralLocation = CLLocation(latitude: mapView.centerCoordinate.latitude, longitude: mapView.centerCoordinate.longitude)
self.centralLocationCoordinate = mapView.centerCoordinate
print("Radius - \(self.getRadius(centralLocation))")
}
func getRadius(centralLocation: CLLocation) -> Double{
let topCentralLat:Double = centralLocation.coordinate.latitude - mapView.region.span.latitudeDelta/2
let topCentralLocation = CLLocation(latitude: topCentralLat, longitude: centralLocation.coordinate.longitude)
let radius = centralLocation.distanceFromLocation(topCentralLocation)
return radius / 1000.0 // to convert radius to meters
}
To account for both landscape and portrait orientations, and/or situations where the map orientation is close to Northeast, Northwest, Southwest, Southeast, and to enclose the full screen up to the corners, one should consider both latitudeDelta and longitudeDelta:
func getRadius() -> Double{
let centralLocation = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude)
let cornerOfMap = CLLocation(latitude: centralLocation.coordinate.latitude + mapView.region.span.latitudeDelta , longitude: centralLocation.coordinate.longitude + mapView.region.span.longitudeDelta)
let radius = centralLocation.distance(from: cornerOfMap)
return radius / 1000.0 // to convert radius to meters
}

MKMapView - how to calculate radius consistent with set-up

I draw a circle on an MKMapView. But how do I calculate the radius of this circle such that it equals the value of the radius used to draw the circle in the first place?
func setArea(_ center:CLLocationCoordinate2D, radius:CLLocationDistance) {
let area = MKCircle(center:center, radius:radius / LocationViewController.kRadiusInset)
mapView.setVisibleMapRect(mapView.mapRectThatFits(area.boundingMapRect), animated:false)
}
I've tried the following but it returns a value very slightly larger than radius passed to setArea. It calculates the distance from the center of the map to the left-hand edge.
// getRadius
let distance = middle.distance(from: edge)
let middle = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude)
let edge = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.centerCoordinate.longitude - (mapView.region.span.longitudeDelta * 0.5))
let distance = middle.distance(from: edge)
If I pass in '4000' metres to setArea() and then afterwards calculate the map's radius (say, within mapViewDidFinishRenderingMap() I get 4010.61219348448
Why the discrepancy?
I replaced the use of a MKCircle with the following:
let region = MKCoordinateRegionMakeWithDistance(center, radius * 2, radius * 2)
mapView.region = region
This is less inaccurate. With radius == 4000 fed into this I get 4000.00048143541 after I set my MKMapView's region. This difference will creep into the user-interface but not nearly as quickly as my first approach.
Can anyone get closer?

Displaying MKAnnotationViews that are within the bounds of the visible mapview

I'm querying my server for a list of locations that are closest to the user based on his observable portion of the mapview.
I'm not entirely sure on how to do this, but should I send the center lat and long of the mapview's visible region and then search for results based on nearest radius?
I know that MKMapView has two properties called region and visibleMapRect. Should I use one of those, and if so, which one is more appropriate based on my question?
EDIT
I'm looking to implement functionality that's identical to the apple maps and Yelp app when you search for nearby locations and it shows you what's relevant based off the visible portion of the map view.
EDIT 2
I have seen a lot of people break up the visible portion of their mapview into quadrants, most commonly NW,NE,SW and SE. However, I'm still not entirely sure why theyr'e doing this. I would like to know the best way to query the back end, which contains a lat and long for each location, to find the locations which exist on the mapview.
I have looked everywhere on stackoverflow and I found a similar question here. However, it doesn't answer my question and there's no mention of visibleMapRect because it's over 5 years old.
Any help is tremendously appreciated.
So you would need to provide your server call with these 2 coord bottomleft and topright 4 points all together.
if lat of location > bottomleft.lat and if lat of location < topright.lat
if long of location > bottomleft.long and if long of location < topright.long
iOS Code would look like this the make server call with these four variables as parameters
fun viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Using region
var span: MKCoordinateSpan = mapView.region.span
var center: CLLocationCoordinate2D = mapView.region.center
// This is the farthest Lat point to the left
var farthestLeft = center.latitude - span.latitudeDelta * 0.5
// This is the farthest Lat point to the Right
var farthestRight = center.latitude + span.latitudeDelta * 0.5
// This is the farthest Long point in the Upward direction
var farthestTop = center.longitude - span.longitudeDelta * 0.5
// This is the farthest Long point in the Downward direction
var farthestBottom = center.longitude + span.longitudeDelta * 0.5
var SWCoord = MKCoordinateForMapPoint(farthestBottom, farthestLeft)
var NECoord = MKCoordinateForMapPoint(farthestTop, farthestRight)
// Using visibleMapRect
var mapRect = mapView.visibleMapRect
// This is the top right Coordinate
var NECoord = getCoordinateFromMapRectanglePoint(MKMapRectGetMaxX(mapRect), y: mapRect.origin.y)
// This is the bottom left Coordinate
var SWCoord = getCoordinateFromMapRectanglePoint(mapRect.origin.x, y: MKMapRectGetMaxY(mapRect))
// Not needed but could be useful
// var NWCoord = getCoordinateFromMapRectanglePoint(MKMapRectGetMinX(mapRect), y: mapRect.origin.y)
// var SECoord = getCoordinateFromMapRectanglePoint(MKMapRectGetMaxX(mapRect), y: MKMapRectGetMaxY(mapRect))
}
func getCoordinateFromMapRectanglePoint(x: Double, y: Double) -> CLLocationCoordinate2D {
var mapPoint = MKMapPointMake(x, y)
return MKCoordinateForMapPoint(mapPoint)
}
If you have anymore questions just comment
EDIT 2 I have seen a lot of people break up the visible portion of their mapview into quadrants, most commonly NW,NE,SW and SE.
However, I'm still not entirely sure why theyr'e doing this. I would
like to know the best way to query the back end, which contains a lat
and long for each location, to find the locations which exist on the
mapview.
I'm not sure if this is what you're referring to but they could be using quadtrees as a way to efficiently search the locations of a huge amount of coordinates.
This post from Thoughtbot demonstrates how to use quadtrees to display thousands of place coordinates as clusters on the map and it includes gifs that very succinctly explain how it works:
A quad tree is a data structure comprising nodes which store a bucket of points and a bounding box. Any point which is contained within the node’s bounding box is added to its bucket. Once the bucket gets filled up, the node splits itself into four nodes, each with a bounding box corresponding to a quadrant of its parents bounding box. All points which would have gone into the parent’s bucket now go into one of its children’s buckets.
Source: How To Efficiently Display Large Amounts of Data on iOS Maps

Best practice for using lat/long within a UIView (not MKMapView)

Basically i have a list of POI's (name,lat,long) and i want to draw them on the UIView, relative to my current lat/long. I'm looking for some best practice for mapping these POI (lat/long) to a UIView.
I don't want to use MKMapView (no need for displaying map-data).
I was reading:
http://developer.apple.com/library/ios/#documentation/general/conceptual/Devpedia-CocoaApp/CoordinateSystem.html
But I'm clueless how i get from a CLLocation to a (x,y) on my UIView. I only want to draw those POI's around my current location. So, for example if my screen would represent a 20 by 30 KM region, how do i map my POI's to their corresponding (x,y) coordinates?
Thanks.
What you're doing is a little strange, but you can convert latitude/longitude to a CGPoint-like struct called an MKMapPoint. An MKMapPoint has an x and y value which correspond to points on a map. Imagine if you laid out a flat map of the world, and 0,0 was the top left. MKMapPoint is a point on that map using that coordinate system.
Use the function MKMapPointForCoordinate() to convert a CLLocationCoordinate2D to an MKMapPoint
MKMapPoint myMapPoint = MKMapPointForCoordinate(myLocationCoordinate);
When you get the list of points, you'll have to do something like finding the max and min x and y values, then fitting all the points into your view using those values, otherwise you'll end up with a load of very close points in one place in your view.
My guess is that, for a 20KM by 30KM region, you can consider the earth to be flat and there fore linearly extrapolate the coordinates. I am sure you can google and find out as to how much distance is a difference in 0.00001 in latitude and longitude.
So if you have 20Km to be represented on X axis, and your current location is 30.1234567 in latitude, and 0.0000001 is 1 km then you can put your coordinate in the center of the screen and 30.1234557 as the left most X coordinate and so on.
I am not trying to provide an answer here, but just trying to think out loud, because I wanted to do some thing similar as well and did it as an Internet based app (without display though), where given two coordinates, I had to find the distance between them.
There are many (many) different approaches to modelling the planet and translating 3D coordinates onto a 2D surface, and the errors introduced by the various methods vary depending on what part of the globe you are. This question seems to cover most of what you are after though:
Converting Longitude & Latitude to X Y on a map with Calibration points
I think its best way (correctly work for Mercator projection map):
extension UIView
{
func addLocation(coordinate: CLLocationCoordinate2D)
{
// max MKMapPoint values
let maxY = Double(267995781)
let maxX = Double(268435456)
let mapPoint = MKMapPointForCoordinate(coordinate)
let normalizatePointX = CGFloat(mapPoint.x / maxX)
let normalizatePointY = CGFloat(mapPoint.y / maxY)
let pointView = UIView(frame: CGRectMake(0, 0, 5, 5))
pointView.center = CGPointMake(normalizatePointX * frame.width, normalizatePointY * frame.height)
pointView.backgroundColor = UIColor.blueColor()
addSubview(pointView)
}
}
My simple project for adding coordinate on UIView: https://github.com/Glechik/MapCoordinateDrawer

Resources