iOS MapKit - Detect when map is fully zoomed in? - ios

So I am using a clustering library to group annotations and there is a small bug with it whereby some very close together annotations can appear grouped when the map is fully zoomed in. With this being a framework I can't do much about it directly but I can disable all grouping if the map is fully zoomed in. The problem is I can't work out a reliable way of doing this.
Here is my regionDidChangeAnimated code which is ideally where I would like to check if the map is fully zoomed in (to the point where you cant zoom in any more).
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
NSOperationQueue().addOperationWithBlock {
let scale: Double = Double(self.map.bounds.size.width) / self.map.visibleMapRect.size.width
let annotations = self.clusteringManager?.clusteredAnnotationsWithinMapRect(self.map.visibleMapRect, withZoomScale: scale)
self.clusteringManager?.displayAnnotations(annotations, onMapView: self.map)
}
}
I have tried inspecting the mapView.region.span property but I'm sure this will change depending on screen size etc...
Any suggestions? Thanks in advance.

You need to extend your MKMapView:
class YourMapView : MKMapView {
// function returns current zoom level of the map
func getCurrentZoom() -> Double {
var angleCamera = self.camera.heading
if angleCamera > 270 {
angleCamera = 360 - angleCamera
} else if angleCamera > 90 {
angleCamera = fabs(angleCamera - 180)
}
let angleRad = M_PI * angleCamera / 180
let width = Double(self.frame.size.width)
let height = Double(self.frame.size.height)
let offset : Double = 20 // offset of Windows (StatusBar)
let spanStraight = width * self.region.span.longitudeDelta / (width * cos(angleRad) + (height - offset) * sin(angleRad))
return log2(360 * ((width / 256) / spanStraight)) + 1;
}
}
Now your able to read out the current Zoom Level out in the following delegate methods:
regionDidChangeAnimated
And
regionWillChangeAnimated

Related

Why is my CGPoint returning other characters?

Hi there I am trying to add a label to the centre of the thumb on a custom UIControl. I need to find the centre on the thumb.
I have a function that I'd like to return the thumb centre as CGPoint.
The function is returning really strange coordinates that look like:
thumb center coordinates: (5.534284385e-315, 5.356796015e-315)
Why am I getting these values?
Does UIControl have a way to return thumbRect like UISlider? Thank you!!
open func getThumbCenterRect() -> CGPoint {
return thumbCenter
}
fileprivate var thumbCenter: CGPoint {
var thumbCenter = viewCenter
let angle = rtlAwareAngleRadians(thumbAngle)
thumbCenter.x += CGFloat(cos(angle)) * controlRadius
thumbCenter.y += CGFloat(sin(angle)) * controlRadius
return thumbCenter
}
#IBAction func onSlideChange(_ sender: MTCircularSlider) {
let thumbCenter = knobWithLabelView.getThumbCenterRect()
print("thumb centre coordinates: \(thumbCenter)")
}
output:
thumb coordinates: (5.534284385e-315, 5.356796015e-315)
It's scientific notation. 5.534284385e-315 means 5.534284385 * 10^-315, which is a really small number (a 0., followed by 314 zeroes, followed by 55342...`).
In other words, it's a really small number, that's really close to 0 but not quite. Most probably, you'll want to do some rounding to get values that snap to more reasonable accepted values.

SpriteKit stop spinning wheel in a defined angle

I have a spinning wheel rotating at an angular speed ω, no acceleration involved, implemented with SpriteKit.
When the user push a button I need to slowly decelerate the wheel from the current angle ∂0 and end-up in a specified angle (lets call it ∂f).
I created associated to it a mass of 2.
I already tried the angularDamping and the SKAction.rotate(toAngle: duration:) but they do not fit my needs because:
With the angularDamping I cannot specify easy the angle ∂f where I want to end up.
With the SKAction.rotate(toAngle: duration:) I cannot start slowing down from the current rotation speed and it doesn't behave natural.
The only remaining approach I tried is by using the SKAction.applyTorque(duration:).
This sounds interesting but I have problems calculating the formula to obtain the correct torque to apply and especially for the inertia and radius of the wheel.
Here is my approach:
I'm taking the starting angular velocity ω as:
wheelNode.physicsBody?.angularVelocity.
I'm taking the mass from wheelNode.physicsBody?.mass
The time t is a constant of 10 (this means that in 10 seconds I want the wheel decelerating to the final angle ∂f).
The deceleration that I calculated as:
let a = -1 * ω / t
The inertia should be: let I = 1/2 * mass * pow(r, 2)*. (see notes regarding the radius please)
Then, finally, I calculated the final torque to apply as: let t = I * a (taking care that is opposite of the current angular speed of the wheel).
NOTE:
Since I don't have clear how to have the radius of the wheel I tried to grab it both from:
the wheelNode.physicsBody?.area as let r = sqrt(wheelNode.physicsBody?.area ?? 0 / .pi)
by converting from pixel to meters as the area documentation says. Then I have let r = self.wheelNode.radius / 150.
Funny: I obtain 2 different values :(
UNFORTUNATLY something in this approach is not working because so far I have no idea how to end up in the specified angle and the wheel doesn't stop anyway as it should (or the torque is too much and spins in the other direction, or is not enough). So, also the torque applied seems to be wrong.
Do you know a better way to achieve the result I need? Is that the correct approach? If yes, what's wrong with my calculations?
Kinematics makes my head hurt, but here you go. I made it to where you can input the amount of rotations and the wheel will rotate that many times as its slowing down to the angle you specify. The other function and extension are there to keep the code relatively clean/readable. So if you just want one giant mess function go ahead and modify it.
• Make sure the node's angularDampening = 0.0
• Make sure the node has a circular physicsbody
// Stops a spinning SpriteNode at a specified angle within a certain amount of rotations
//NOTE: Node must have a circular physicsbody
// Damping should be from 0.0 to 1.0
func decelerate(node: SKSpriteNode, toAngle: CGFloat, rotations: Int) {
if node.physicsBody == nil { print("Node doesn't have a physicsbody"); return } //Avoid crash incase node's physicsbody is nil
var cw:CGFloat { if node.physicsBody!.angularVelocity < CGFloat(0.0) { return -1.0} else { return 1.0} } //Clockwise - using int to reduce if statments with booleans
let m = node.physicsBody!.mass // Mass
let r = CGFloat.squareRoot(node.physicsBody!.area / CGFloat.pi)() // Radius
let i = 0.5 * m * r.squared // Intertia
let wi = node.physicsBody!.angularVelocity // Initial Angular Velocity
let wf:CGFloat = 0 // Final Angular Velocity
let ti = CGFloat.unitCircle(node.zRotation) // Initial Theta
var tf = CGFloat.unitCircle(toAngle) // Final Theta
//Correction constant based on rate of rotation since there seems to be a delay between when the action is calcuated and when it is run
//Without the correction the node stops a little off from its desired stop angle
tf -= 0.00773889 * wi //Might need to change constn
let dt = deltaTheta(ti, tf, Int(cw), rotations)
let a = -cw * 0.5 * wi.squared / abs(dt) // Angular Acceleration - cw used to determine direction
print("A:\(a)")
let time:Double = Double(abs((wf-wi) / a)) // Time needed to stop
let torque:CGFloat = i * a // Torque needed to stop
node.run(SKAction.applyTorque(torque, duration: time))
}
func deltaTheta(_ ti:CGFloat, _ tf:CGFloat, _ clockwise: Int, _ rotations: Int) -> CGFloat {
let extra = CGFloat(rotations)*2*CGFloat.pi
if clockwise == -1 {
if tf>ti { return tf-ti-2*CGFloat.pi-extra }else{ return tf-ti-extra }
}else{
if tf>ti { return tf-ti+extra }else{ return tf+2*CGFloat.pi+extra-ti }
}
}
}
extension CGFloat {
public var squared:CGFloat { return self * self }
public static func unitCircle(_ value: CGFloat) -> CGFloat {
if value < 0 { return 2 * CGFloat.pi + value }
else{ return value }
}
}

iPhone calculate numbers between 0-50 for custom slider

I want to calculate numbers between 0 to 50 when user move on the slider right and left. I am using a formula such as but didn't succedded.
(point.x + 100) / 20
Where point.x is a CGPoint getting with locatioOfView: method between two UIView's
CGPoint point = [sender locationInView:self.viewBaseSlider];
Note : Here self.viewBaseSlider is not a slider it is a Custom View.
Any ideas?
You have to translate location from range between 0 to your-custom-view-width to range from 0 to your-slider-max (50 in this case).
Here's my take on it in swift:
let maxPoints = 50
let customViewWidth = customView.frame.size.width
let pointWidth = Float(customViewWidth) / Float(maxPoints)
if let touch = touches.first {
let location = touch.location(in: customView)
let point = Int(roundf(Float(location.x) / pointWidth))
print("Selected \(point)")
}
And here's working example, where grey area is customView and output in console comes from clicking in the start, around the middle and the end of the grey region:

CALayer drawinContext called # 60fps but view update graphics # 1fps

I am trying to implement a graph drawing view in OSX using Cocoa and Quartz framework using NSBezierPath and add/delete data points as I go.
Doing so in drawRect worked fine as the graph was updating frequently but then I encountered performance problem when I need to increase total datapoints/sampling rate.
I decided to move to drawLayer: inContext: but as the function is called at 60fps, the view isn't updating the graph when the function is call and instead update at 1fps.
What am I doing wrong here?
class CustomDrawLayer: CALayer {
convenience init(view: NSView, drawsAsynchronously : Bool = false) {
self.init()
self.bounds = view.bounds
self.anchorPoint = CGPointZero
self.opaque = false
self.frame = view.frame
self.drawsAsynchronously = drawsAsynchronously
// for multiple draws in hosting view
// self.delegate = self
}
override func actionForLayer(layer: CALayer, forKey event: String) -> CAAction? {
return nil
}}
override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
if layer == self.layer {
Swift.print("axes drawing")
graphBounds.origin = self.frame.origin
graphAxes.drawAxesInRect(graphBounds, axeOrigin: plotOrigin, xPointsToShow: CGFloat(totalSecondsToDisplay), yPointsToShow: CGFloat(totalChannelsToDisplay))
}
if layer == self.board {
Swift.print(1/NSDate().timeIntervalSinceDate(fpsTimer))
fpsTimer = NSDate()
drawPointsInGraph(graphAxes, context: ctx)
}
}
func drawPointsInGraph(axes: AxesDrawer, context: CGContext)
{
color.set()
var x : CGFloat = 0
var y : CGFloat = 0
for var channel = 0; channel < Int(totalChannelsToDisplay); channel++ {
path.removeAllPoints()
var visibleIndex = (dirtyRect.origin.x - axes.position.x) / (axes.pointsPerUnit.x / samplingRate)
if visibleIndex < 2 {
visibleIndex = 2
}
for var counter = Int(visibleIndex); counter < dataStream![channel].count; counter++ {
if dataStream![channel][counter] == 0 {
if path.elementCount > 0 {
path.stroke()
}
break
}
let position = axes.position
let ppY = axes.pointsPerUnit.y
let ppX = axes.pointsPerUnit.x
let channelYLocation = CGFloat(channel)
x = position.x + CGFloat(counter-1) * (ppX / samplingRate)
y = ((channelYLocation * ppY) + position.y) + (dataStream![channel][counter-1] * (ppY))
path.moveToPoint(CGPoint(x: align(x), y: align(y)))
x = position.x + CGFloat(counter) * (ppX / samplingRate)
y = ((channelYLocation * ppY) + position.y) + (dataStream![channel][counter] * (ppY) )
path.lineToPoint(CGPoint(x: align(x), y: align(y)))
if x > (axes.position.x + axes.bounds.width) * 0.9 {
graphAxes.forwardStep = 5
dirtyRect = graphBounds
for var c = 0; c < Int(totalChannelsToDisplay); c++ {
for var i = 0; i < Int(samplingRate) * graphAxes.forwardStep; i++
{
dataStream![c][i] = 0
}
}
return
}
}
path.stroke()
}
if inLiveResize {
dirtyRect = graphBounds
} else {
dirtyRect.origin.x = x
dirtyRect.origin.y = bounds.minY
dirtyRect.size.width = 10
dirtyRect.size.height = bounds.height
}
}
It is incredibly rare that you should ever call a function at 60 Hz. In no case should you ever try to call a drawing function at 60 Hz; that never makes sense in Cocoa. If you really mean "at the screen refresh interval," see CADisplayLink, which is specifically built to allow you to draw at the screen refresh interval. This may be slower than 60 Hz. If you try to draw exactly at 60 Hz, you can get out of sync and cause beats in your animation. But this really only intended for things like real-time video. If that what you have, then this is the tool, but it doesn't really sound like it.
It's a bit difficult to understand your code. It's not clear where your 60fps comes in. But I'm assuming what you're trying to do is animate drawing the graph. If so, as Mark F notes, see CAShapeLayer. It has automatic path animations built-in, and is definitely what you want. It automatically handles timings and syncing with the screen refresh and GPU optimizations, and lots of other things that you shouldn't try to work around.
Even if CAShapeLayer isn't what you want, you should be looking at Core Animation, which is designed to work with you to animate values and redraw as necessary. It automatically will handle rendering your layer on multiple cores for instance, which will dramatically improve performance. For more on that, see Animating Custom Layer Properties.
If your path needs to be drawn that frequently, check out CAShapeLayer, where you can just change the path property. That will be hardware accelerated and much faster than drawRect or drawLayer.

Move view within superview's bounds (iOS)

In an app I am attempting to have a view move within another view to a random location. I have been able to do this as follows:
mySmallerView.center = randomizeLocation()
func randomizeLocation() -> CGPoint {
let randomX = arc4random_uniform(UInt32(mainView.frame.width))
let randomY = arc4random_uniform(UInt32(mainView.frame.height))
return CGPointMake(CGFloat(randomX), CGFloat(randomY))
}
This moves it around quite nicely, BUT it uses center, so sometimes, the left is off the screen, or the right, top and/or bottom go off the screen because its center can be pushing the limits of the view's frame.
How would I improve randomizeLocation() to where it would ensure the BOUNDS/FRAME of the view to be moved do not exceed the bounds/frame of its superview?
Is there a way to do this?
i think it should be like this the max randomX must not able to be the mianView.frame.width
func randomizeLocation() -> CGPoint {
let randomX = arc4random_uniform(UInt32(mainView.frame.width - (mySmallerView.frame.width))) + (mySmallerView.frame.width / 2)
let randomY = arc4random_uniform(UInt32(mainView.frame.height - (mySmallerView.frame.height))) + (mySmallerView.frame.height / 2)
}
I think you need to make sure you don't put your smaller view too far out or down (by adding - (mySmallerView.frame.width / 2)). You also want to make sure you don't go too far to the left or up (by adding + (mySmallerView.frame.width / 2)).
func randomizeLocation() -> CGPoint {
let randomX = arc4random_uniform(UInt32(mainView.frame.width - (mySmallerView.frame.width / 2))) + (mySmallerView.frame.width / 2)
let randomY = arc4random_uniform(UInt32(mainView.frame.height - (mySmallerView.frame.height / 2))) + (mySmallerView.frame.height / 2)
return CGPointMake(CGFloat(randomX), CGFloat(randomY))
}

Resources