How to offset MKMapView to put coordinates under specified point - ios

I have two view controllers. One allows the user to enter their own sightings of things:
The other is a UITableViewController that allows them to see their submitted sightings:
What I want is for the user to be able to click on their sighting in the 2nd VC and for it to populate the 1st VC with the relevant details. I have that working for the textFields using :
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let pvc = self.navigationController?.previousViewController() as! SubmitSightingVC
let sighting = sightingsArray[indexPath.row]
pvc.titleTextField.text = sighting["title"] as! String?
pvc.locationTextField.text = sighting["location"] as! String?
pvc.dateTextField.text = sighting["date"] as! String?
pvc.descriptionTextView.text = sighting["sightingDesc"] as! String?
let pinArray = ["1", "2", "3", "4", "5"]
let pinTextArray = ["1text", "2text", "3text", "4text", "5text"]
var pinImageArray = [#imageLiteral(resourceName: "1"), #imageLiteral(resourceName: "2"), #imageLiteral(resourceName: "3"), #imageLiteral(resourceName: "4"), #imageLiteral(resourceName: "5")]
let pinIconString = sighting["pinIcon"] as! String
let index = pinArray.index(of: pinIconString)
pvc.pinImageView.image = pinImageArray[index!]
pvc.pinTextField.text = pinTextArray[index!]
// Bit I'm struggling with:
var coordinates = CLLocationCoordinate2D(latitude: sighting["latitude"] as! Double, longitude: sighting["longitude"] as! Double)
pvc.mapView.centerCoordinate = coordinates
self.navigationController!.popViewController(animated: true)
}
The problem is that because the centre of the map is behind the blurred view it is offset. The pin on the map isn't an actual pin but a UIImage (pinImageView). How can I move the map so that the coordinates are under this pinImage instead of in the centre of the mapView?

As mentioned above you can use the MKMapCamera to position the view of the map or you can also use setVisibleMapRect and add some padding to position the map like this:
self.mapView.setVisibleMapRect(self.mapView.visibleMapRect, edgePadding: UIEdgeInsetsMake(200.0, 0.0, 0.0, 0.0), animated: true)
This will offset the center by 200 screen pixels on the top.

From the excellent #rmp answer, the general idea is
self.map.setVisibleMapRect(self.map.visibleMapRect,
edgePadding: UIEdgeInsets(top: -200, left: 0, bottom: 0, right: 0),
animated: true)
to move it "upwards 200". However, note that the arithmetic is not really correct.
To move a "box upwards" you have to negative inset the top, and positive inset the bottom. The literal above code will sort of stretch and zoom the box (the center would move up, but it would move up a random height less than 200).
So what you have to do is this:
let UP = 170 // move it up 170
let LEFT = 50 // move it left 50
self.map.setVisibleMapRect(self.map.visibleMapRect,
edgePadding: UIEdgeInsets(top: -UP, left: -LEFT, bottom: UP, right: LEFT),
animated: true)
That will work.
Here's a total formula for getting a point on the map, perhaps a town or house coords and
1. nicely centering it, regardless of current camera position, angle, zoom etc, and then
2. move it, let's say upwards, to an exact point on the screen (to allow for an input display or whatever)
let coords = .. the coords of the town, house etc
// for example, if you let someone tap on the map, something like...
let coords = map.convert(g.location(in: map), toCoordinateFrom: map)
// we'll need the W/H of the map on the device:
let W = self.map.bounds.width
let H = self.map.bounds.height
// here's the formula to tidily and safely move/zoom
// to center a coords on the screen
let thatRegion = MKCoordinateRegion(
center: coords,
latitudinalMeters: 5000 * Double((H/W)),
longitudinalMeters: 5000)
MKMapView.animate(withDuration: 2, animations: { [weak self] in
self?.map.region = thatRegion
}, completion: {completed in
print("part 1 done")
// so now the coords is centered. but we want to move it
// so that we can see something else we're adding to the
// screen on top, such as an input display
.. so that's the first part.
Now you want to continue animating the move to nudge it out of the way of your UX:
MKMapView.animate(withDuration: 2, animations: { [weak self] in
guard let self = self else { return }
// in this example, we'll move the point,
// to the top-left of the screen, just below the notch:
let UP = H/2 - 20
let LEFT = W/4
// here's the exact formula for moving
// the center of the map a certain amount UP/LEFT:
self.map.setVisibleMapRect(self.map.visibleMapRect,
edgePadding: UIEdgeInsets(
top: -UP, left: -LEFT, bottom: UP, right: LEFT),
animated: true)
}, completion: {completed in
print("part 2 done")
})
})
That will always do it.
Also, recall that unfortunately MapKit is inherently sloppy. It is, simply, not possible to position a geographic point absolutely in the center of the screen; there's always a little variation. (Say the odd few pixels one way or the other.) It's just how MapKit works. Also, animations (such as the two above) can be thrown off a bit if the texture data is still loading.

Related

How to fix random rectangle sprite appearing on screen in Xcode Swift game

I am creating a sprite-kit Swift Xcode game. The game has coins in the levels and I used a method learned from an online instructor of using red color sprite boxes to add coins in them. The method worked, and now I have coins replacing the place of what used to be the red color sprite.
In the game, I added SKPhysics to every sprite in it. As soon as I ran the project, a random transparent box sprite appeared, adjacent to the position of the coins (I know because my ball bumped into it and stopped and also that white lines formed around it because I let the game show the physics on the screen).
I've tried changing the anchor point of the red box because I thought it was a problem of positioning but that didn't work. Below is the code I used for adding the coins on the screen
Where I called it (the default of a switch statement of adding physics to every special sprite depending on their name in it) :
switch name {
case "Enemy":
PhysicsHelper.addPhysicsBody(to: sprite, with: name)
default:
let component = name.components(separatedBy: NSCharacterSet.decimalDigits.inverted)
if let rows = Int(component[0]), let columns = Int(component[1]) {
calculateGridWith(rows: rows, columns: columns, parent: sprite)
}
}
}
The functions:
static func calculateGridWith(rows: Int, columns: Int, parent: SKSpriteNode) {
parent.color = UIColor.clear
for x in 0..<columns {
for y in 0..<rows {
let position = CGPoint(x: x, y: y)
addCoin(to: parent, at: position, columns: columns)
}
}
}
static func addCoin(to parent: SKSpriteNode, at position: CGPoint, columns: Int) {
let coin = SKSpriteNode(imageNamed: GameConstants.StringConstants.coinImageName)
coin.size = CGSize(width: parent.size.width/CGFloat(columns), height: parent.size.width/CGFloat(columns))
coin.name = GameConstants.StringConstants.coinName
coin.position = CGPoint(x: position.x * coin.size.width + coin.size.width/2, y: position.y * coin.size.height + coin.size.height/2)
let coinFrames = AnimationHelper.loadTextures(from: SKTextureAtlas(named: GameConstants.StringConstants.coinRotateAtlas), withName: GameConstants.StringConstants.coinPrefixKey)
coin.run(SKAction.repeatForever(SKAction.animate(with: coinFrames, timePerFrame: 0.1)))
PhysicsHelper.addPhysicsBody(to: coin, with: GameConstants.StringConstants.coinName)
parent.addChild(coin)
}
The functions for removing the coins after touching it:
func handleCollectible(sprite: SKSpriteNode) {
switch sprite.name! {
case GameConstants.StringConstants.coinName:
collectCoin(sprite: sprite)
default:
break
}
}
func collectCoin(sprite: SKSpriteNode) {
coins += 1
if let coinDust = ParticleHelper.addParticleEffect(name: GameConstants.StringConstants.coinDustEmitterKey, particlePositionRange: CGVector(dx: 5.0, dy: 5.0), position: CGPoint.zero) {
coinDust.zPosition = GameConstants.ZPositions.objectZ
sprite.addChild(coinDust)
sprite.run(SKAction.fadeOut(withDuration: 0.4), completion: {
coinDust.removeFromParent()
sprite.removeFromParent()
})
}
}
I've expected coins to replace the red box I drew in the SKS file. But the result is coins replacing the red box and a transparent SKPhysicsBody/sprite appearing on the screen adjacent to the coins.
Does anyone know how to fix this bug?
Thanks!

ARKit - initially placing object in ARSKView at a certain heading/angle from camera

I'm creating my anchor and 2d node in ARSKView like so:
func displayToken(distance: Float) {
print("token dropped at: \(distance)")
guard let sceneView = self.view as? ARSKView else {
return
}
// Create anchor using the camera's current position
if let currentFrame = sceneView.session.currentFrame {
removeToken()
// Create a transform with a translation of x meters in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -distance
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
// Create and configure a node for the anchor added to the view's session.
if let image = tokenImage {
let texture = SKTexture(image: image)
let tokenImageNode = SKSpriteNode(texture: texture)
tokenImageNode.name = "token"
return tokenImageNode
} else {
return nil
}
}
This places it exactly in front of the camera at a given distance (z). What I want to also do is take a latitude/longitude for an object, calculate an angle or heading in degrees, and initially drop the anchor/node at this angle from the camera. I'm currently getting the heading by using the GMSGeometryHeading method, which takes users current location, and the target location to determine the heading. So when dropping the anchor, I want to put it in the right direction towards the target location's lat/lon. How can I achieve this with SpriteKit/ARKit?
Can you clarify your question a bit please?
Perhaps the following lines can help you as example. There the cameraNode moves using a basic geometry-formula, moving obliquely depending on the angle (in Euler) in both x and y coordinates
var angleEuler = 0.1
let rotateX = Double(cameraNode.presentation.position.x) - sin(degrees(radians: Double(angleEuler)))*10
let rotateZ = Double(cameraNode.presentation.position.z) - abs(cos(degrees(radians: Double(angleEuler))))*10
cameraNode.position = SCNVector3(x:Float(rotateX), y:0, z:Float(rotateX))
If you want an object fall in front of the camera, and the length of the fall depend on a degree just calculate the value of "a" in the geometry-formula "Tan A = a/b" and update the node's "presentation.position.y"
I hope this helps

Moving SKNode by One Tile From Its Current Position

I'm playing with the tile editor, with which I've created a simple, somewhat large map like the following. The tile size is 64 x 64. And the map size is 58 x 32.
And I want to know the current position of the game character. Let me call it player (as SKNode). I want to know this guy's current position so that I can find out whether any of its adjascent tile is NOT an obstacle. The following lines of code gives me (7, -3).
background = self.childNode(withName: "World") as! SKTileMapNode
let position = background.convert(player.position, from: player)
let column = background.tileColumnIndex(fromPosition: position)
let row = background.tileRowIndex(fromPosition: position)
print(column, row)
, which suggests that these coordinates point to the center of the camera, indicated by the red circle. The following gives me (10, 2).
let position = background.convert(player.position, from: self)
let column = background.tileColumnIndex(fromPosition: position)
let row = background.tileRowIndex(fromPosition: position)
That's the position from the bottom-left corner.
Now, if I want to move the game character up by 1 tile, writing...
let position = background.convert(player.position, from: player)
let column = background.tileColumnIndex(fromPosition: position)
let row = background.tileRowIndex(fromPosition: position)
let destination = background.centerOfTile(atColumn: column, row: row + 1)
let action = SKAction.move(to: destination, duration: 1)
player.run(action)
, the game character will go missing in action, disappearing from the face of the map. So how can I move the node by one tile?
Maybe try this:
let currentPlayerPosition = background.convert(player.position, from: player)
let column = background.tileColumnIndex(fromPosition: currentPlayerPosition)
let row = background.tileRowIndex(fromPosition: currentPlayerPosition)
let destination = background.centerOfTile(atColumn: column, row: row + 1)
// the new line I added
let newPlayerPosition = background.convert(destination, to: player)
let action = SKAction.move(to: newPlayerPosition, duration: 1)
player.run(action)

In each cell in UITableViewController I have embedded map. Is there a way of changing it to screenshots of map instead?

In my ios swift app I have a UITableViewController with cells added dynamically. Each cell has a MKMapView embedded and I'm setting the center of the map for each cell on different coordinates. I'm doing so by calling this method:
func centerMapOnLocation(location: CLLocation, map: MKMapView, radius: CLLocationDistance) {
let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate,
radius * 2.0, radius * 2.0)
map.setRegion(coordinateRegion, animated: true)
}
inside cellForRowAtIndexPath:
override func tableView(tview: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tview.dequeueReusableCellWithIdentifier("cell") as! SingleCall
let user:SingleUser = self.items[indexPath.row] as! SingleUser
let regionRadius: CLLocationDistance = 150
let initialLocation = CLLocation(latitude: user.coordinate.latitude, longitude: user.coordinate.longitude)
centerMapOnLocation(initialLocation, map: cell.eventMap, radius: regionRadius)
cell.eventMap.zoomEnabled = false
cell.eventMap.scrollEnabled = false
cell.eventMap.userInteractionEnabled = false
}
This is fine and it works, but I imagine with lots of records there will be problems with memory - even now with only couple cells when user scrolls the table - each map loads instantly and I can only imagine how much compute power it takes to do so.
So my question is - is there a way of changing the dynamic map view to some kind of maybe screenshot of the actual position of the map? Will it work faster when it comes to lots of cells?
Yes, it is possible. In order to do so, you should switch to using a UIImageView (called mapImageView in the code below) and MKMapSnapshotter. To make things really fly, you should cache the images that MKMapSnapshotter produces so that they load instantly when a user scrolls back to a cell that was seen before. This approach is gentle on resources since it only uses the one global MKMapSnapShotter rather than a map for each cell.
Here's some code that I've adapted to your situation. I haven't included the detail on how to cache specifically, since that depends on how you want to handle caching in your app. I've used the Haneke cocoapod: https://cocoapods.org/pods/Haneke in the past. Also, I've set many of the common snapshotted options below, but you should probably adapt them to your use case.
//CHECK FOR CACHED MAP, IF NOT THEN GENERATE A NEW MAP SNAPSHOT
let coord = CLLocationCoordinate2D(latitude: placeLatitude, longitude: placeLongitude)
let snapshotterOptions = MKMapSnapshotOptions()
snapshotterOptions.scale = UIScreen.mainScreen().scale
let squareDimension = cell.bounds.width < cell.bounds.height ? (cell.bounds.width)! : (cell.bounds.height)!
snapshotterOptions.size = CGSize(width: squareDimension, height: squareDimension)
snapshotterOptions.mapType = MKMapType.Hybrid
snapshotterOptions.showsBuildings = true
snapshotterOptions.showsPointsOfInterest = true
snapshotterOptions.region = MKCoordinateRegionMakeWithDistance(coord, 5000, 5000)
let snapper = MKMapSnapshotter(options: snapshotterOptions)
snapper.startWithCompletionHandler({ (snap, error) in
if(error != nil) {
print("error: " + error!.localizedDescription)
return
}
dispatch_async(dispatch_get_main_queue()) {
guard let snap = snap where error == nil else { return }
if let indexPaths = self.tview.indexPathsForVisibleRows {
if indexPaths.contains(indexPath) {
cell.mapImageView.image = snap
}
}
//SET CACHE HERE
}
})

Issues with map view camera heading

I am having issues with the order/way that I'm setting up my UIMapView. This is what I would like to happen:
View appears - Map rotates to specified heading
Reset button tapped - If user has moved the map, it will reset to the default heading and zoom
At the moment the map is rotated to the heading when the map appears, but the reset button does nothing. I suspect this is down to the order I am doing things because if I flip two lines of code around, it works, but it doesn't rotate to the correct heading when the map appears.
Here is my code:
#IBAction func rotateToDefault(sender: AnyObject) {
mapView.setRegion(zoomRegion, animated: true)
mapView.camera.heading = parkPassed.orientation!
}
override func viewWillAppear(animated: Bool) {
setUpMapView()
}
override func viewDidAppear(animated: Bool) {
mapView.setRegion(zoomRegion, animated: true)
mapView.camera.heading = parkPassed.orientation!
}
func setUpMapView() {
rideArray = ((DataManager.sharedInstance.rideArray) as NSArray) as! [Ride]
zoomRegion = MKCoordinateRegionMakeWithDistance(CLLocationCoordinate2D(latitude: parkPassed.latitude!, longitude: parkPassed.longitude!), 1000, 1000)
mapView.setRegion(zoomRegion, animated: true)
mapView.delegate = self
for ride in rideArray {
var subtitle = ""
if locationManager.location == nil {
subtitle = "Distance unavailable"
} else {
let userLocation = CLLocation(latitude: locationManager.location.coordinate.latitude, longitude: locationManager.location.coordinate.longitude)
let annotationLocation = CLLocation(latitude: ride.latitude!, longitude: ride.longitude!)
var distance = Int(CLLocationDistance(annotationLocation.distanceFromLocation(userLocation)))
if distance > 1000 {
distance = distance / 1000
subtitle = "\(distance) kilometers"
} else {
subtitle = "\(distance) meters"
}
}
let annotation = RideAnnotation(coordinate: CLLocationCoordinate2DMake(ride.latitude!, ride.longitude!), title: ride.name!, subtitle: subtitle)
self.qTree.insertObject(annotation)
annotationsAdded.insertObject(annotation, atIndex: 0)
println(qTree.count)
}
}
Anyone have any suggestions?
I had a similar problem.
It appears region and camera are two mutual exclusive concepts to determine how you see which part of the map.
If you use region, you have coordinate and span to determine what you see (you already did this in your code)
If you use camera, you have coordinate, distance, pitch and heading to determine how you see the map.
use mapView.setCamera(...) to smoothly change what you see, including the heading.
To define your camera view you do something like
let camera = MKMapCamera(lookingAtCenterCoordinate: userLocation, fromDistance: 1000, pitch: 0, heading: heading)
self.mapView.setCamera(camera, animated: false)
From Apples Documentation:
Assigning a new camera to this property updates the map immediately and without animating the change. If you want to animate changes in camera position, use the setCamera:animated: method instead.

Resources