I'm building a mobile app using the Google Maps SDK for iOS and I'm trying to use the mobile device's gyroscope data to pan the camera around a panorama in Street View. I've setup a GMSPanoramaView and a GMSPanoramaCamera with initial positions. I'm using the method -updateCamera on GMSPanoramaView but am unable to smoothly pan across each panorama. If anyone has any idea how I can achieve this feature please let me know. Here is my code so far in the -viewDidLoad portion of my viewcontroller:
if manager.gyroAvailable {
let queue = NSOperationQueue.mainQueue()
manager.startGyroUpdatesToQueue(queue, withHandler: { (data, error) -> Void in
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
// Update UI
let cameraUpdate = GMSPanoramaCameraUpdate.rotateBy((data?.rotationRate.x.radiansToDegrees)!)
self.panoView.updateCamera(cameraUpdate, animationDuration: 1)
})
})
}
Following code is working in swift 3
if motionManager.isGyroAvailable {
motionManager.startGyroUpdates(to: OperationQueue.main, withHandler: { (gyroData: CMGyroData?, error: Error?) in
let y = gyroData!.rotationRate.y
print("gyrodata: \(y)")
let cameraUpdate = GMSPanoramaCameraUpdate.rotate(by: -CGFloat((gyroData?.rotationRate.y)!))
panoView.updateCamera(cameraUpdate, animationDuration: 1)
})
}
Working code in Swift 4 (controls both left right and up down so wherever the phone points is where the view will be updated)
import GoogleMaps
import CoreMotion
class ViewController: UIViewController, GMSPanoramaViewDelegate {
let motionManager = CMMotionManager()
override func loadView() {
let panoView = GMSPanoramaView(frame: .zero)
panoView.delegate = self
self.view = panoView
// you can choose any latitude longitude here, this is my random choice
panoView.moveNearCoordinate(CLLocationCoordinate2D(latitude: 48.858, longitude: 2.284))
motionManager.startDeviceMotionUpdates()
if motionManager.isGyroAvailable {
motionManager.startGyroUpdates(to: OperationQueue.main, withHandler: { (gyroData: CMGyroData?, error: Error?) in
// needed to figure out the rotation
let y = (gyroData?.rotationRate.y)!
let motion = self.motionManager.deviceMotion
if(motion?.attitude.pitch != nil) {
// calculate the pitch movement (up / down) I subtract 40 just as
// an offset to the view so it's more at face level.
// the -40 is optional, can be changed to anything.
let pitchCamera = GMSPanoramaCameraUpdate.setPitch( CGFloat(motion!.attitude.pitch).radiansToDegrees - 40 )
// rotation calculation (left / right)
let rotateCamera = GMSPanoramaCameraUpdate.rotate(by: -CGFloat(y) )
// rotate camera immediately
panoView.updateCamera(pitchCamera, animationDuration: 0)
// for some reason, when trying to update camera
// immediately after one another, it will fail
// here we are dispatching after 1 millisecond for success
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0001, execute: {
panoView.updateCamera(rotateCamera, animationDuration: 0)
})
}
})
}
}
}
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
And do not forget to put this in your info.plist
Privacy - Motion Usage Description
Add a proper descriptor for why you need this data in the info.plist and that should properly configure your application.
I have easy way, in my project i use "CLLocationManagerDelegate" - func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading)
self.locationManager.startUpdatingHeading() // put this line on viewdidload for example
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
self.panoramaView.animate(to: GMSPanoramaCamera(heading: newHeading.magneticHeading, pitch: newHeading.y, zoom: 1), animationDuration: 0.1)
}
Related
I got some Code from the Mapbox Tutorials for a basic Navigation App in a Turn -by- Turn View. Everything is working fine, and I've already added a Waypoint.
In the example the origin of the route is set with fixed coordinates. That's not compatible with my Use Case. The Waypoints and the Destination are also fixed by coordinates, which is good. But the origin has to be the "Location of the User", which is obviously variable.
Maybe someone could help me, would be much appreciated. :)
import Foundation
import UIKit
import MapboxCoreNavigation
import MapboxNavigation
import MapboxDirections
class PlacemarktestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let origin = CLLocationCoordinate2DMake(37.77440680146262, -122.43539772352648)
let waypoint = CLLocationCoordinate2DMake(27.76556957793795, -112.42409811526268)
let destination = CLLocationCoordinate2DMake(37.76556957793795, -122.42409811526268)
***strong text***
let options = NavigationRouteOptions(coordinates: [origin, waypoint, destination])
Directions.shared.calculate(options) { [weak self] (session, result) in
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let response):
guard let route = response.routes?.first, let strongSelf = self else {
return
}
// For demonstration purposes, simulate locations if the Simulate Navigation option is on.
// Since first route is retrieved from response `routeIndex` is set to 0.
let navigationService = MapboxNavigationService(route: route, routeIndex: 0, routeOptions: options)
let navigationOptions = NavigationOptions(navigationService: navigationService)
let navigationViewController = NavigationViewController(for: route, routeIndex: 0, routeOptions: options, navigationOptions: navigationOptions)
navigationViewController.modalPresentationStyle = .fullScreen
// Render part of the route that has been traversed with full transparency, to give the illusion of a disappearing route.
navigationViewController.routeLineTracksTraversal = true
strongSelf.present(navigationViewController, animated: true, completion: nil)
}
}
}
}
#zeno, to set user location you can use
mapView.setUserTrackingMode(.follow, animated: true)
don't forget to add NSLocationWhenInUseUsageDescription key to info plist.
Or you can get coordinates and set them manually using CoreLocation
import CoreLocation
let locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestLocation() // Request a user’s location
Documentation is here requestLocation
And then implement the CLLocationManagerDelegate method locationManager(:, didUpdateLocations:) to handle the requested user location. This method will be called once after using locationManager.requestLocation().
func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
if let location = locations.first {
let latitude = location.coordinate.latitude
let longitude = location.coordinate.longitude
}
}
In my project, I have used CoreMotion, my code is in img. sometimes it crashed, but I can't repeat it, and I also don't know why.. could someone help me? I have been tortured by it for a long time...
startMotionManager:
Please try this code
import CoreMotion
var motionManager: CMMotionManager?
override func viewDidLoad() {
super.viewDidLoad()
motionManager = CMMotionManager()
if motionManager?.isDeviceMotionAvailable == true {
motionManager?.deviceMotionUpdateInterval = 0.1;
let queue = OperationQueue()
motionManager?.startDeviceMotionUpdates(to: queue, withHandler: { [weak self] (motion, error) -> Void in
// Get the attitude of the device
if let attitude = motion?.attitude {
// Get the pitch (in radians) and convert to degrees.
// Import Darwin to get M_PI in Swift
print(attitude.pitch * 180.0/M_PI)
}
})
print("Device motion started")
}
else {
print("Device motion unavailable");
}
// Do any additional setup after loading the view, typically from a nib.
}
based on attitude.pitch you can know Device angle and rotation on ( + & - )
I have an iPhone application with a level in it that is based on the gravityY parameter of a device motion call to motionmanager. I have fixed the level to the pitch of the phone, as I wish to show the user whether the phone is elevated or declined relative to a flat plane (flat to the ground) through its x-axis...side to side or rotated is not relevant. To do that, I have programmed the app to slide an indicator (red when out of level) along the level (a bar)....its maximum travel is each end of the level.
The level works great...and a correct value is displayed, until the user locks the phone and puts it in his or her back pocket. While in this stage, the level indicator shifts to one end of the level (the end of the phone that is elevated in the pocket), and when the phone is pulled out and unlocked, the app does not immediately restore the level - it remains out of level, even if I do a manual function call to restore the level. After about 5 minutes, the level seems to restore itself.
Here is the code:
func getElevation () {
//now get the device orientation - want the gravity value
if self.motionManager.isDeviceMotionAvailable {
self.motionManager.deviceMotionUpdateInterval = 0.05
self.motionManager.startDeviceMotionUpdates(
to: OperationQueue.current!, withHandler: {
deviceMotion, error -> Void in
var gravityValueY:Double = 0
if(error == nil) {
let gravityData = self.motionManager.deviceMotion
let gravityValueYRad = (gravityData!.gravity.y)
gravityValueY = round(180/(.pi) * (gravityValueYRad))
self.Angle.text = "\(String(round(gravityValueY)))"
}
else {
//handle the error
self.Angle.text = "0"
gravityValueY = 0
}
var elevationY = gravityValueY
//limit movement of bubble
if elevationY > 45 {
elevationY = 45
}
else if elevationY < -45 {
elevationY = -45
}
let outofLevel: UIImage? = #imageLiteral(resourceName: "levelBubble-1")
let alignLevel: UIImage? = #imageLiteral(resourceName: "levelBubbleGR-1")
let highElevation:Double = 1.75
let lowElevation:Double = -1.75
if highElevation < elevationY {
self.bubble.image = outofLevel
}
else if elevationY < lowElevation {
self.bubble.image = outofLevel
}
else {
self.bubble.image = alignLevel
}
// Move the bubble on the level
if let bubble = self.bubble {
UIView.animate(withDuration: 1.5, animations: { () -> Void in
bubble.transform = CGAffineTransform(translationX: 0, y: CGFloat(elevationY))
})
}
})
}
}
I would like the level to restore almost immediately (within 2-3 seconds). I have no way to force calibration or an update. This is my first post....help appreciated.
Edit - I have tried setting up a separate application without any animation with the code that follows:
//
import UIKit
import CoreMotion
class ViewController: UIViewController {
let motionManager = CMMotionManager()
#IBOutlet weak var angle: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func startLevel(_ sender: Any) {
startLevel()
}
func startLevel() {
//now get the device orientation - want the gravity value
if self.motionManager.isDeviceMotionAvailable {
self.motionManager.deviceMotionUpdateInterval = 0.1
self.motionManager.startDeviceMotionUpdates(
to: OperationQueue.current!, withHandler: {
deviceMotion, error -> Void in
var gravityValueY:Double = 0
if(error == nil) {
let gravityData = self.motionManager.deviceMotion
let gravityValueYRad = (gravityData!.gravity.y)
gravityValueY = round(180/(.pi) * (gravityValueYRad))
}
else {
//handle the error
gravityValueY = 0
}
self.angle.text = "(\(gravityValueY))"
})}
}
}
Still behaves exactly the same way....
OK....so I figured this out through trial and error. First, I built a stopGravity function as follows:
func stopGravity () {
if self.motionManager.isDeviceMotionAvailable {
self.motionManager.stopDeviceMotionUpdates()
}
}
I found that the level was always set properly if I called that function, for example by moving to a new view, then restarting updates when returning to the original view. When locking the device or clicking the home button, I needed to call the same function, then restart the gravity features on reloading or returning to the view.
To do that I inserted the following in the viewDidLoad()...
NotificationCenter.default.addObserver(self, selector: #selector(stopGravity), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(stopGravity), name: NSNotification.Name.UIApplicationWillTerminate, object: nil)
This notifies the AppDelegate and runs the function. That fixed the issue immediately.
I can fetch Heading data from CLLocationManager.
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.headingFilter = 0.2
locationManager.headingOrientation = CLDeviceOrientation.landscapeRight
locationManager.startUpdatingHeading()
locationManager.delegate = self
I can use
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
let headingDegree = newHeading.trueHeading
// Keep update CALayer
scrollLayer.scroll(to: CGPoint(x: headingDegree, y: 20.0))
}
to update UI. The UI is a heading tap on the top of a CAScrollLayer.
Problem:
The heading tape keeps shaking when the iPhone yawing too fast. I believe the scrolling activity is not quick enough to process overwhelming heading data.
Question:
Is there any better way to handle heading update data with CAScrollLayer?
It sounds like you need some smoothing in place. To be clear, smoothing (in it's most basic form) is taking an average value over time. Let's say that you want to take the average value over the last 60 or so updates then you would have an array of values which you use to calculate the average:
var arr: [Double] = []
func average(latestVal: Double) -> Double
{
if arr.count >= 60 {
arr.remove(at: 0)
}
arr.append(latestVal)
let total = arr.reduce(0, { $0 + $1 })
return total / Double(arr.count)
}
This really is the perfect opportunity to implement a queue in Swift which you can find details on here.
I have been doing some iOS development for a couple of months and recently I am developing a bus app.
I am currently mimicking the bus' movements and set up multiple annotations on the bus stops. For test purposes, I have setup just one bus stop and am trying to monitor when the bus has entered this region and exited as well.
Strangely, my didStartMonitoringForRegion method is called perfectly but neither the didEnterRegion nor didExitRegion methods are called. Every time I run the program, the bus pretty much passes the stop without prompting me so.
Could someone explain to me why this is happening and how to resolve it?
let locationManager = CLLocationManager()
var allBusAnnotations = [MKPointAnnotation]()
var summitEastBusStations = [CLLocationCoordinate2D]()
var busStopNames = ["Dix Stadium", "Risman Plaza", "Terrace Drive", "Terrace Drive 2","C-Midway","Theatre Dr.","East Main Street","South Lincoln"]
var radius = 500 as CLLocationDistance
// 0.02 is the best zoom in factor
var mapZoomInFactor : Double = 0.02
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.getBusStop()
self.locationManager.delegate = self
// gets the exact location of the user
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
// gets the user's location only when the app is in use and not background
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
self.mapView.showsUserLocation = true
self.setBusStopAnnotations(summitEastBusStations)
// self.mapView.mapType = MKMapType.Satellite
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
// sends the latitude and longitude to the Apple Servers then returns the address
CLGeocoder().reverseGeocodeLocation(manager.location, completionHandler: { (placeMarks: [AnyObject]!, error: NSError!) -> Void in
if error != nil
{
println("Reverse Geocode Failed: " + error.localizedDescription)
return
}
if placeMarks.count > 0
{
// gets the most updated location
let pm = placeMarks.last as! CLPlacemark
let centre = CLLocationCoordinate2D(latitude: manager.location.coordinate.latitude, longitude: manager.location.coordinate.longitude)
// draws a circle in which the map will zoom to
let region = MKCoordinateRegion(center: centre, span: MKCoordinateSpan(latitudeDelta: self.mapZoomInFactor, longitudeDelta: self.mapZoomInFactor))
self.mapView.setRegion(region, animated: true)
self.displayLocationInfo(pm)
// self.distanceToClosestAnnotation(pm)
self.geoFencing()
// YOU CAN IGNORE THIS WHOLE PART. IT'S IRRELEVANT FOR THIS QUESTION
var repeatTimes = 0
var count = 0
while(count <= 7)
{
if count == (self.summitEastBusStations.count - 1)
{
count = 1
++repeatTimes
}
else if repeatTimes == 1
{
count = 0
++repeatTimes
}
else if repeatTimes == 2
{
break
}
self.distanceToBusStop(pm, count: count)
++count
}
}
})
}
func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) {
println("Location Manager Failed: " + error.localizedDescription)
}
func locationManager(manager: CLLocationManager!, didStartMonitoringForRegion region: CLRegion!) {
println("The region is monitored")
println("The monitored region is \(region.description)")
}
func locationManager(manager: CLLocationManager!, monitoringDidFailForRegion region: CLRegion!, withError error: NSError!) {
println("Failed to monitor the stated region")
}
func locationManager(manager: CLLocationManager!, didEnterRegion region: CLRegion!) {
println("The bus has entered the region")
}
func locationManager(manager: CLLocationManager!, didExitRegion region: CLRegion!) {
println("The bus has left the region")
}
func geoFencing()
{
let rismanPlaza = CLLocationCoordinate2D(latitude: 41.1469492, longitude: -81.344068)
var currentBusStop = CLLocation(latitude: rismanPlaza.latitude, longitude: rismanPlaza.longitude)
addRadiusCircle(currentBusStop)
let busStopRegion = CLCircularRegion(center: CLLocationCoordinate2D(latitude: rismanPlaza.latitude, longitude: rismanPlaza.longitude), radius: radius, identifier: busStopNames[1])
if radius > self.locationManager.maximumRegionMonitoringDistance
{
radius = self.locationManager.maximumRegionMonitoringDistance
}
locationManager.startMonitoringForRegion(busStopRegion)
}
// creates the radius around the specified location
func addRadiusCircle(location: CLLocation)
{
self.mapView.delegate = self
var circle = MKCircle(centerCoordinate: location.coordinate, radius: radius)
self.mapView.addOverlay(circle)
}
// performs the actual circle colouring
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer!
{
if overlay is MKCircle
{
var circle = MKCircleRenderer(overlay: overlay)
circle.strokeColor = UIColor.redColor()
circle.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.1)
circle.lineWidth = 1
return circle
}
else
{
return nil
}
}
I ended up using the CLRegion.containsCoordinate(location.coordinate) method instead. It works pretty much the same way.
Once the object has entered my set region, it returns true and from here I can know when it's entered and exited the region.
Please ensure [CLLocationManager regionMonitoringAvailable] returns YES and
CLLocationManager.monitoredRegions contains valid regions.
Also, from Apple documentation:
In iOS 6, regions with a radius between 1 and 400 meters work better
on iPhone 4S or later devices. (In iOS 5, regions with a radius
between 1 and 150 meters work better on iPhone 4S and later devices.)
On these devices, an app can expect to receive the appropriate region
entered or region exited notification within 3 to 5 minutes on
average, if not sooner.
And
Apps can expect a notification as soon as the device moves 500 meters
or more from its previous notification. It should not expect
notifications more frequently than once every five minutes. If the
device is able to retrieve data from the network, the location manager
is much more likely to deliver notifications in a timely manner.
There are many causes why your delegates are not triggering. First goto target settings and in capabilities tab check whether in BackgroundModes Location Updates is enabled.
If it is on then try to check whether your current location manager holds the region you've specified by checking
NSLog(#"Monitored Regions %#",self.locationManager.monitoredRegions);
Then if the user device is at current location(Latitude and longitude) the didEnterRegion: and didExitRegion: delegates will not be triggering. Use didDetermineState: method to find whether the user/device is in current region that is moniotred. If it so, didDetermineState: will be triggered. Once the user leaves the region didExitRegion: will be triggered.
Then after also if delegates aren't trigerring then find then error using the following in delegate
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error {
NSLog(#"Failed to Monitor %#", error);
}