rubymotion/BubbleWrap

View on GitHub
motion/location/location.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# Provides a nice DSL for interacting with the standard
# CLLocationManager
#
module BubbleWrap
  module CLLocationWrap

    def latitude
      self.coordinate.latitude
    end

    def longitude
      self.coordinate.longitude
    end
  end

  module Location
    module Error
      DISABLED=0
      PERMISSION_DENIED=1
      NETWORK_FAILURE=2
      LOCATION_UNKNOWN=3
    end

    Constants.register KCLLocationAccuracyBestForNavigation, KCLLocationAccuracyBest,
        KCLLocationAccuracyNearestTenMeters, KCLLocationAccuracyHundredMeters,
        KCLLocationAccuracyKilometer, KCLLocationAccuracyThreeKilometers

    module_function
    # Start getting locations
    # @param [Hash] options = {
    #   authorization_type: :always/:when_in_use to trigger the type of authorization you want
    #     default == uses :always
    #   significant: true/false; whether to listen for significant location changes or
    #     all location changes (see Apple docs for info); default == false
    #   distance_filter:  minimum change in distance to be updated about, in meters;
    #     default == uses KCLDistanceFilterNone,
    #   desired_accuracy: minimum accuracy for updates to arrive;
    #     any of :best_for_navigation, :best, :nearest_ten_meters,
    #     :hundred_meters, :kilometer, or :three_kilometers; default == :best
    #   purpose: string to display when the system asks user for location,
    #   retries: if location cant be found. how many errors do we retry; default == 5
    #   calibration: if the OS should display the heading calibration to the user; default == false
    # }
    # @block for callback. takes one argument, `result`.
    #   - On error or cancelled, is called with a hash {error: BW::Location::Error::<Type>}
    #   - On success, is called with a hash {to: #<CLLocation>, from: #<CLLocation>, previous: [#<CLLocation>,...]}
    #   -- :previous will return an Array of CLLocation objects, ordered from oldest to newest, excluding the 
    #        locations :to and :from, returning an empty Array if no additional locations were provided
    #
    # Example
    # BW::Location.get(distance_filter: 10, desired_accuracy: :nearest_ten_meters) do |result|
    #   result[:to].class == CLLocation
    #   result[:from].class == CLLocation
    #   result[:previous].class == NSArray<CLLocation>
    #   p "Lat #{result[:to].latitude}, Long #{result[:to].longitude}"
    # end
    def get(options = {}, &block)
      @callback = block
      @callback.weak! if @callback && BubbleWrap.use_weak_callbacks?
      @options = {
        authorization_type: :always,
        significant: false,
        distance_filter: KCLDistanceFilterNone,
        desired_accuracy: KCLLocationAccuracyBest,
        retries: 5,
        once: false,
        calibration: false
      }.merge(options)

      @options[:significant] = false if @options[:significant].nil?
      @retries = 0
      @from_location = nil

      if not enabled?
        error(Error::DISABLED) and return
      end

      self.location_manager

      if self.location_manager.respondsToSelector('requestAlwaysAuthorization')
        @options[:authorization_type] == :always ? self.location_manager.requestAlwaysAuthorization : self.location_manager.requestWhenInUseAuthorization
      end


      self.location_manager.distanceFilter = @options[:distance_filter]
      self.location_manager.desiredAccuracy = Constants.get("KCLLocationAccuracy", @options[:desired_accuracy])
      self.location_manager.purpose = @options[:purpose] if @options[:purpose]

      @initialized = true
      start
    end

    def get_significant(options = {}, &block)
      get(options.merge(significant: true), &block)
    end

    # Get the first returned location based on your options
    # @param [Hash] options = {
    #   significant: true/false; whether to listen for significant location changes or
    #     all location changes (see Apple docs for info); default == false
    #   distance_filter:  minimum change in distance to be updated about, in meters;
    #     default == uses KCLDistanceFilterNone,
    #   desired_accuracy: minimum accuracy for updates to arrive;
    #     any of :best_for_navigation, :best, :nearest_ten_meters,
    #     :hundred_meters, :kilometer, or :three_kilometers; default == :best
    #   purpose: string to display when the system asks user for location,
    #   retries: if location cant be found. how many errors do we retry; default == 5
    # }
    # @block for callback. takes one argument, `result`.
    #   - On error or cancelled, is called with a hash {error: BW::Location::Error::<Type>}
    #   - On success, it returns a CLLocation
    #
    #
    # Example
    # BW::Location.get_once(desired_accuracy: :three_kilometers, purpose: 'We need to use your GPS to show you how fun RM is') do |result|
    #   if result.is_a?(CLLocation)
    #     p "Lat #{result.latitude}, Long #{result.longitude}"
    #   else
    #     p "ERROR: #{result[:error]"
    #   end
    # end
    def get_once(options = {}, &block)
      get(options.merge(once: true), &block)
    end

    def get_compass(options = {}, &block)
      get(options.merge(compass: true), &block)
    end

    def get_compass_once(options = {}, &block)
      get_compass(options.merge(once: true), &block)
    end

    # Start getting locations
    def start
      return unless initialized?
      if @options[:significant]
        self.location_manager.startMonitoringSignificantLocationChanges
      elsif @options[:compass]
        self.location_manager.startUpdatingHeading
      else
        self.location_manager.startUpdatingLocation
      end
    end

    # Stop getting locations
    def stop
      return unless @options
      if @options[:significant]
        self.location_manager.stopMonitoringSignificantLocationChanges
      elsif @options[:compass]
        self.location_manager.stopUpdatingHeading
      else
        self.location_manager.stopUpdatingLocation
      end
    end

    def location_manager
      @location_manager ||= CLLocationManager.alloc.init
      @location_manager.delegate ||= self
      @location_manager
    end

    # returns true/false whether services, or limited services, are enabled for the _device_
    def enabled?
      CLLocationManager.locationServicesEnabled
    end

    # returns true/false if CLLocationManager has been initialized with the provided or default options
    def initialized?
      @initialized ||= false
    end

    # returns true/false whether services are enabled for the _app_
    def authorized?
      [
        BW::Constants.get("KCLAuthorizationStatus", :authorized)
      ].include?(CLLocationManager.authorizationStatus)
    end

    def error(type)
      @callback && @callback.call({ error: type })
      @callback = nil
      self.location_manager.stopUpdatingLocation
    end

    ##########
    # CLLocationManagerDelegate Methods
    def locationManager(manager, didUpdateLocations:locations)
      if @options[:once]
        @callback && @callback.call(locations.last)
        @callback = proc { |result| }
        stop
      else
        size = locations.count
        result = {to: locations.last, 
          from: ( (size > 1) ? locations.last(2).first : @from_location ), 
          previous: ( (size > 2) ? locations.first(size - 2) : [] )
        }
        @from_location = result[:to]
        @callback && @callback.call(result)
      end
    end

    def locationManager(manager, didUpdateHeading:newHeading)
      heading = {
        magnetic_heading: newHeading.magneticHeading,
        true_heading: newHeading.trueHeading,
        accuracy: newHeading.headingAccuracy,
        timestamp: newHeading.timestamp,
      }

      if @options[:once]
        @callback && @callback.call(heading)
        @callback = proc { |result| }
        stop
      else
        @callback && @callback.call(heading)
      end
    end

    def locationManager(manager, didFailWithError:error)
      if error.domain == KCLErrorDomain
        case error.code
        when KCLErrorDenied
          error(Error::PERMISSION_DENIED)
        when KCLErrorLocationUnknown
          # Docs specify that this is a temporary error,
          # so we stop/start updating to try again.
          @retries += 1
          if @retries > @options[:retries]
            error(Error::LOCATION_UNKNOWN)
          else
            stop
            start
          end
        when KCLErrorNetwork
          error(Error::NETWORK_FAILURE)
        end
      end
    end

    def locationManager(manager, didChangeAuthorizationStatus:status)
      case status
      when KCLAuthorizationStatusRestricted
        error(Error::PERMISSION_DENIED)
      when KCLAuthorizationStatusDenied
        error(Error::PERMISSION_DENIED)
      end
    end

    def locationManagerShouldDisplayHeadingCalibration(manager)
      @options[:calibration] ? @options[:calibration] : false
    end
  end
end
::Location = BubbleWrap::Location unless defined?(::Location)