Check if user location is inside a shape - ios

I made this method to check if an user location is inside a polygon on a map view (mapkit).
I pass to the method the current user location (CLLocationCoordinate2D) and return a boolean just to know if the user is in a polygon or not.
func userInsidePolygon(userlocation: CLLocationCoordinate2D ) -> Bool{
// get every overlay on the map
let o = self.mapView.overlays
// loop every overlay on map
for overlay in o {
// handle only polygon
if overlay is MKPolygon{
let polygon:MKPolygon = overlay as! MKPolygon
let polygonPath:CGMutablePathRef = CGPathCreateMutable()
// get points of polygon
let arrPoints = polygon.points()
// create cgpath
for (var i:Int=0; i < polygon.pointCount; i++){
let mp:MKMapPoint = arrPoints[i]
if (i == 0){
CGPathMoveToPoint(polygonPath, nil, CGFloat(mp.x), CGFloat(mp.y))
}
else{
CGPathAddLineToPoint(polygonPath, nil, CGFloat(mp.x), CGFloat(mp.y))
}
}
let mapPointAsCGP:CGPoint = self.mapView.convertCoordinate(userlocation, toPointToView: self.mapView)
return CGPathContainsPoint(polygonPath , nil, mapPointAsCGP, false)
}
}
return false
}
I don't really understand why, but the user is never inside a polygon after this test. (and i'm pretty sure he is)
I think it's possible that i have a logic problem with lat/long against x,y.
Does anybody already have work with like this ?
Thanks in advance for all suggestions.
Cheers

The problem is that you are converting the userLocation from the map coordinates to the view coordinates but when you build the path, you don't convert the points to the views coordinates.
You'll need to convert the MKMapPoint to a CLLocationCoordinate2D then to a CGPoint.
let polygonMapPoint: MKMapPoint = arrPoints[i]
let polygonCoordinate = MKCoordinateForMapPoint(polygonPoint)
let polygonPoint self.mapView.convertCoordinate(polygonPointAsCoordinate, toPointToView: self.mapView)
Then use polygonPoint when building the path
CGPathMoveToPoint(polygonPath, nil, polygonPoint.x, polygonPoint.y)

Swift 3, Xcode 8 answer:
func userInsidePolygon(userlocation: CLLocationCoordinate2D ) -> Bool {
var containsPoint: Bool = false
// get every overlay on the map
let o = self.mapView.overlays
// loop every overlay on map
for overlay in o {
// handle only polygon
if overlay is MKPolygon{
let polygon:MKPolygon = overlay as! MKPolygon
let polygonPath:CGMutablePath = CGMutablePath()
// get points of polygon
let arrPoints = polygon.points()
// create cgpath
for i in 0..<polygon.pointCount {
let polygonMapPoint: MKMapPoint = arrPoints[i]
let polygonCoordinate = MKCoordinateForMapPoint(polygonMapPoint)
let polygonPoint = self.mapView.convert(polygonCoordinate, toPointTo: self.mapView)
if (i == 0){
polygonPath.move(to: CGPoint(x: polygonPoint.x, y: polygonPoint.y))
}
else{
polygonPath.addLine(to: CGPoint(x: polygonPoint.x, y: polygonPoint.y))
}
}
let mapPointAsCGP:CGPoint = self.mapView.convert(userlocation, toPointTo: self.mapView)
containsPoint = polygonPath.contains(mapPointAsCGP)
if containsPoint {
return true
}
}
}
return containsPoint
}

Related

Why is my programmatic screenshot capturing out of date view?

I have a map view that allows users to draw perimeter lines, and I need to capture a screenshot when they are done to save for recording purposes. For some reason, my code is capturing the view state before the new overlay is added, even though I add the overlay before attempting the screenshot.
I use a separate view to capture gesture, and then covert the points to an overlay and add it to the map here where points are the points gathered from the gesture tracking
func convertFragments() {
var coordinates: [CLLocationCoordinate2D] = []
for point in points {
let coordinate = mapView.convert(point, toCoordinateFrom: drawingView)
coordinates.append(coordinate)
}
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
polyline.title = selectedTool.name
removeLines(for: selectedTool)
points = []
mapView.addOverlay(polyline)
incidentManager.update(for: selectedTool, value: polyline)
}
Then incidentManager.update(value: polyline) makes a network call to save the information and calls a second method to capture the screenshot and update a log in log(eventType: eventType)
func update(for tool: MapTool, value: Any) {
func saveLine(_ line: MKPolyline, for key: String) {
incidentReference.setData([
key: IncidentManager.convertCLPoints(line.coordinates)
], merge: true)
}
switch tool {
case .hotZone, .innerPerimeter, .outerPerimeter:
let line = value as! MKPolyline
saveLine(line, for: tool.rawValue)
case .commandPost, .stagingArea:
let point = value as! CLLocationCoordinate2D
let geoPoint = GeoPoint(latitude: point.latitude, longitude: point.longitude)
incidentReference.setData([
tool.rawValue: geoPoint
], merge: true)
case .poi:
let annotation = value as! PerimeterMapAnnotation
let point = annotation.coordinate
let geoPoint = GeoPoint(latitude: point.latitude, longitude: point.longitude)
incidentReference.setData([
"pointsOfInterest": [annotation.title: geoPoint]
], merge: true)
default:
return
}
updateAddress(defaultValue: value)
let eventType = tool.eventType(didSet: true)
log(eventType: eventType)
}
I have an extension on UIView that captures the view, but for some reason, its not the most updated version of the map view.
private func log(eventType: LogEventType) {
guard let mapImage = mapView.asImage() else { return }
let storageRef = Storage.storage().reference()
let imageRef = storageRef.child("\(incident.id)/\(UUID().uuidString).jpg")
let eventTime = Date()
let eventTimeString = eventTime.apiDateString
incidentReference.collection("eventLog").document(eventTimeString).setData([
"logEventType": eventType.rawValue,
"imageReference": imageRef.fullPath,
"date": Timestamp(date: eventTime)
])
upload(image: mapImage, at: imageRef)
if shouldUpdateCover {
shouldUpdateCover = false
incidentReference.setData([
"coverPhotoRef": imageRef.fullPath
], merge: true)
}
}
func asImage() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0.0)
defer { UIGraphicsEndImageContext() }
if let context = UIGraphicsGetCurrentContext() {
self.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
return nil
}
I have tried many variations this "screenshot" method that I have found online, and no luck. When I debug, I can check the map and its overlays, and see that they are updating before hand.
Any ideas why the image captured here does not capture the changes made?

How to draw a CLLocationCoordinate2Ds on MKMapSnapshotter (drawing on mapView printed image)

I have mapView with array of CLLocationCoordinate2D. I use these locations to draw lines on my mapView by using MKPolyline. Now i want to store it as a UIimage. I found that theres class MKMapSnapshotter but unfortunately i can't draw overlays on it "Snapshotter objects do not capture the visual representations of any overlays or annotations that your app creates." So i get only blank map image. Is there any way to get image with my overlays?
private func generateImageFromMap() {
let mapSnapshotterOptions = MKMapSnapshotter.Options()
guard let region = mapRegion() else { return }
mapSnapshotterOptions.region = region
mapSnapshotterOptions.size = CGSize(width: 200, height: 200)
mapSnapshotterOptions.showsBuildings = false
mapSnapshotterOptions.showsPointsOfInterest = false
let snapShotter = MKMapSnapshotter(options: mapSnapshotterOptions)
snapShotter.start() { snapshot, error in
guard let snapshot = snapshot else {
//do something with image ....
let mapImage = snapshot...
}
}
}
How can i put overlays on this image? Or maybe theres other way for that problem.
Unfortunately, you have to draw them yourself. Fortunately, MKSnapshot has a convenient point(for:) method to convert a CLLocationCoordinate2D into a CGPoint within the snapshot.
For example, assume you had an array of CLLocationCoordinate2D:
private var coordinates: [CLLocationCoordinate2D]?
private func generateImageFromMap() {
guard let region = mapRegion() else { return }
let options = MKMapSnapshotter.Options()
options.region = region
options.size = CGSize(width: 200, height: 200)
options.showsBuildings = false
options.showsPointsOfInterest = false
MKMapSnapshotter(options: options).start() { snapshot, error in
guard let snapshot = snapshot else { return }
let mapImage = snapshot.image
let finalImage = UIGraphicsImageRenderer(size: mapImage.size).image { _ in
// draw the map image
mapImage.draw(at: .zero)
// only bother with the following if we have a path with two or more coordinates
guard let coordinates = self.coordinates, coordinates.count > 1 else { return }
// convert the `[CLLocationCoordinate2D]` into a `[CGPoint]`
let points = coordinates.map { coordinate in
snapshot.point(for: coordinate)
}
// build a bezier path using that `[CGPoint]`
let path = UIBezierPath()
path.move(to: points[0])
for point in points.dropFirst() {
path.addLine(to: point)
}
// stroke it
path.lineWidth = 1
UIColor.blue.setStroke()
path.stroke()
}
// do something with finalImage
}
}
Then the following map view (with the coordinates, as MKPolyline, rendered by mapView(_:rendererFor:), like usual):
The above code will create the this finalImage:

ARKit - getting distance from camera to anchor

I'm creating an anchor and adding it to my ARSKView at a certain distance in front of the camera like this:
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 {
// 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)
}
}
then the node gets created for the anchor like this:
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 works fine and I see the image get added at the appropriate distance. However, what I'm trying to do is then calculate how far the anchor/node is in front of the camera as you move. The problem is the calculation seems to be off immediately using fabs(cameraZ - anchor.transform.columns.3.z). Please see my code below that is in the update() method to calculate distance between camera and object:
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
guard let sceneView = self.view as? ARSKView else {
return
}
if let currentFrame = sceneView.session.currentFrame {
let cameraZ = currentFrame.camera.transform.columns.3.z
for anchor in currentFrame.anchors {
if let spriteNode = sceneView.node(for: anchor), spriteNode.name == "token", intersects(spriteNode) {
// token is within the camera view
//print("token is within camera view from update method")
print("DISTANCE BETWEEN CAMERA AND TOKEN: \(fabs(cameraZ - anchor.transform.columns.3.z))")
print(cameraZ)
print(anchor.transform.columns.3.z)
}
}
}
}
Any help is appreciated in order to accurately get distance between camera and the anchor.
The last column of a 4x4 transform matrix is the translation vector (or position relative to a parent coordinate space), so you can get the distance in three dimensions between two transforms by simply subtracting those vectors.
let anchorPosition = anchor.transforms.columns.3
let cameraPosition = camera.transform.columns.3
// here’s a line connecting the two points, which might be useful for other things
let cameraToAnchor = cameraPosition - anchorPosition
// and here’s just the scalar distance
let distance = length(cameraToAnchor)
What you’re doing isn’t working right because you’re subtracting the z-coordinates of each vector. If the two points are different in x, y, and z, just subtracting z doesn’t get you distance.
This one is for scenekit, I'll leave it here though.
let end = node.presentation.worldPosition
let start = sceneView.pointOfView?.worldPosition
let dx = (end?.x)! - (start?.x)!
let dy = (end?.y)! - (start?.y)!
let dz = (end?.z)! - (start?.z)!
let distance = sqrt(pow(dx,2)+pow(dy,2)+pow(dz,2))
With RealityKit there is a slightly different way to do this. If you're using the world tracking configuration, your AnchorEntity object conforms to HasAnchoring which gives you a target. Target is an enum of AnchoringComponent.Target. It has a case .world(let transform). You can compare your world transform to the camera's world transform like this:
if case let AnchoringComponent.Target.world(transform) = yourAnchorEntity.anchoring.target {
let theDistance = distance(transform.columns.3, frame.camera.transform.columns.3)
}
This took me a bit to figure out but I figure others that might be using RealityKit might benefit from this.
As mentioned above by #codeman, this is the right solution:
let distance = simd_distance(YOUR_NODE.simdTransform.columns.3, (sceneView.session.currentFrame?.camera.transform.columns.3)!);
3D distance - You can check these utils,
class ARSceneUtils {
/// return the distance between anchor and camera.
class func distanceBetween(anchor : ARAnchor,AndCamera camera: ARCamera) -> CGFloat {
let anchorPostion = SCNVector3Make(
anchor.transform.columns.3.x,
anchor.transform.columns.3.y,
anchor.transform.columns.3.z
)
let cametaPosition = SCNVector3Make(
camera.transform.columns.3.x,
camera.transform.columns.3.y,
camera.transform.columns.3.z
)
return CGFloat(self.calculateDistance(from: cametaPosition , to: anchorPostion))
}
/// return the distance between 2 vectors.
class func calculateDistance(from: SCNVector3, to: SCNVector3) -> Float {
let x = from.x - to.x
let y = from.y - to.y
let z = from.z - to.z
return sqrtf( (x * x) + (y * y) + (z * z))
}
}
And now you can call:
guard let camera = session.currentFrame?.camera else { return }
let anchor = // you anchor
let distanceAchorAndCamera = ARSceneUtils.distanceBetween(anchor: anchor, AndCamera: camera)

How can can I get equidistant location on a MKPolyline?

I need to find equidistant locations on a MKPolyline to add annotations as shown below.
My function to get locations on MKPolyline is given below, I have the values start and end coordinates of Polyline. But the locations are slightly ,moving out of polyline as shown in the image below
My function to find location is
func getEquidistantPoints(from startPoint: CLLocationCoordinate2D, to endPoint: CLLocationCoordinate2D, numberOfPoints: Int) -> [CLLocationCoordinate2D] {
var midPoints: [CLLocationCoordinate2D] = []
var newPoint: CLLocationCoordinate2D = CLLocationCoordinate2DMake(0, 0)
let count = numberOfPoints + 1
let latitudeModifier = (endPoint.latitude - startPoint.latitude) / Double(count)
let longitudeModifier = (endPoint.longitude - startPoint.longitude) / Double(count)
for i in 0..<count {
newPoint.latitude = CLLocationDegrees(startPoint.latitude + (latitudeModifier * Double(i)))
newPoint.longitude = CLLocationDegrees(startPoint.longitude + (longitudeModifier * Double(i)))
midPoints.append(newPoint)
}
return midPoints
}
In viewdidload
let coordinatesArray = getEquidistantPoints(from: sourceCoordinate, to: destinationCoordinate, numberOfPoints: 5)
for coordinate in coordinatesArray {
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
self.mapView.addAnnotation(annotation)
}
How can I solve this error in calculating locations?
The problem is that the earth is round. So your "line" is not a line; it is a curve traced out on the surface of a nominal sphere. You cannot find "equidistant" points along the line using your simple-minded method. You need to use some much more serious math than you are using. This will prove to be far from trivial.

How do I move marker along with moving of Google Map in iOS?

I am displaying a marker in a particular place, along with displaying the current address in the address label on Google Maps.
Now, I want to change the location by moving the Google Map, but the problem is that when I am moving the map, I should simultaneously move the marker along with the map, and I should display the address of that location in the address label.
How can I do that?
I tried this:
let destinationMarker = GMSMarker(position: self.destinationLocation.coordinate)
let image = UIImage(named:"sourcemarker")
destinationMarker.icon = image
destinationMarker.draggable = true
destinationMarker.map = self.viewMap
//viewMap.selectedMarker = destinationMarker
destinationMarker.title = "hi"
destinationMarker.userData = "changedestination"
func mapView(mapView: GMSMapView, didEndDraggingMarker marker: GMSMarker)
{
if marker.userData as! String == "changedestination"
{
self.destinationLocation = CLLocation(latitude: marker.position.latitude, longitude: marker.position.longitude)
self.destinationCoordinate = self.destinationLocation.coordinate
//getAddressFromLatLong(destinationCoordinate)
}
}
// UpdteLocationCoordinate
func updateLocationoordinates(coordinates:CLLocationCoordinate2D) {
if destinationMarker == nil
{
destinationMarker = GMSMarker()
destinationMarker.position = coordinates
let image = UIImage(named:"destinationmarker")
destinationMarker.icon = image
destinationMarker.map = viewMap
destinationMarker.appearAnimation = kGMSMarkerAnimationPop
}
else
{
CATransaction.begin()
CATransaction.setAnimationDuration(1.0)
destinationMarker.position = coordinates
CATransaction.commit()
}
}
// Camera change Position this methods will call every time
func mapView(mapView: GMSMapView, didChangeCameraPosition position: GMSCameraPosition) {
let destinationLocation = CLLocation()
if self.mapGesture == true
{
destinationLocation = CLLocation(latitude: position.target.latitude, longitude: position.target.longitude)
destinationCoordinate = destinationLocation.coordinate
updateLocationoordinates(destinationCoordinate)
}
}
Update for Swift 4:
First you need to conform with the GMSMapViewDelegate:
extension MapsVC: GMSMapViewDelegate{
And then set your VC as the delegate of viewMap in the viewDidLoad()
viewMap.delegate = self
After that you just need to use the following method to get updates from the camera position and set that as the new position for the marker:
func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) {
destinationMarker.position = position.target
print(destinationMarker.position)
}
There is one trick that can help you out here. Instead of using a GMSMarker here, put an image pointing to the center, over your Google MapView.
You can easily find the coordinates of Map's center using this :
double latitude = mapView.camera.target.latitude;
double longitude = mapView.camera.target.longitude;
Or this
GMSCoordinateBounds *bounds = nil;
bounds = [[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
CLLocationCoordinate2D centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
Now you can get the location address by using Geocoding API by google.
Here is the reference :
https://developers.google.com/maps/documentation/ios-sdk/reference/interface_g_m_s_geocoder
You can refresh Address when this delegate method is called :
- (void) mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)position
Hope this helps.
Based from Release Notes made in Maps SDK for IOS, changing a markers position will cause the marker to animate to the new location.
To resolve this, you may use new features for GMSMarker as stated in Release Version 1.5 such as:
Markers can be made draggable using the draggable property, and new drag delegate methods have been added to GMSMapViewDelegate. (Issue 4975)
Added GMSMarkerLayer, a custom CALayer subclass for GMSMarker that supports animation of marker position and rotation. (Issue 4951, Issue 5743)
In addition to that, this post in GitHub - GoogleMapsAnimationGlitch
and this SO post - How to smoothly move GMSMarker along coordinates in Objective c
might also help.
objc_sync_enter(self)
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
guard let `self` = self else { return }
DispatchQueue.main.delay(0.5) {
self.mapViewService.setCenter(location.coordinate, animate: true, zoom: nil)
}
objc_sync_exit(self)
}
CATransaction.setAnimationDuration(vm.continunousLocationUpdateTimeInterval)
self.mapViewService.moveMarker(position: location.coordinate, marker: userMarker)
CATransaction.commit()
the code above is useful to me, the moveMarker method just packaged the code marker.position = newCoordinate

Resources