Custom MKOverlay & Renderer draw not working - swift

I'm attempting to create a custom overlay for MKMapView which varies the colour of the polyline according to certain criteria. However, none of my drawing code works, & I can't for the life of me figure out why. I'm using CoreGPX for trackPoints, but I don't think that's relevant to the problem as the overlay's coordinates are set in the normal way.
The subclasses are -
class FlightOverlay: MKPolyline {
enum Route {
case coordinates([CLLocationCoordinate2D])
case gpx(GPXRoot)
}
var trackPoints: [GPXTrackPoint]?
convenience init?(route: Route) {
switch route {
case .coordinates(let coordinates):
self.init(coordinates: coordinates, count: coordinates.count)
case .gpx(let gpxRoot):
guard let track = gpxRoot.tracks.first, let segment = track.segments.first else {
return nil
}
var trackPoints: [GPXTrackPoint] = []
let coordinates: [CLLocationCoordinate2D] = segment.points.compactMap { (point) -> CLLocationCoordinate2D? in
guard let latitude = point.latitude, let longitude = point.longitude else {
return nil
}
trackPoints.append(point) // this ensures that our trackPoints array & the polyline points have the same number of elements
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
self.init(coordinates: coordinates, count: coordinates.count)
self.trackPoints = trackPoints
}
}
}
class FlightOverlayRenderer: MKPolylineRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let polyline = polyline as? FlightOverlay, let trackPoints = polyline.trackPoints, polyline.pointCount > 1 else {
super.draw(mapRect, zoomScale: zoomScale, in: context)
return
}
context.setLineWidth(lineWidth)
var previousPoint = point(for: polyline.points()[0])
for index in 1..<polyline.pointCount {
let trackPoint = trackPoints[index]
let acceleration: Double
if let accelerationValue = trackPoint.extensions?[.acceleration].text, let accel = Double(accelerationValue) {
acceleration = accel
} else {
acceleration = 0
}
let color = UIColor(red: acceleration * 10, green: 0.5, blue: 0.6, alpha: 1).cgColor
context.setStrokeColor(color)
context.beginPath()
context.move(to: previousPoint)
let point = point(for: polyline.points()[index])
context.addLine(to: point)
context.closePath()
context.drawPath(using: .stroke)
previousPoint = point
}
}
}
Even if I comment out all the stuff which attempts to set colours according to a GPXTrackPoint & just draw a start-to-finish line in bright red, nothing appears. When I directly call super, it works fine with the same coordinates.
Any help much appreciated!

Well, maybe it's a rookie error, but it never occurred to me that I'd need to control the lineWidth as well. Turns out it needs to be adjusted according to the zoomScale - I was drawing the line exactly where it needed to be but it was too thin to be seen. For my case, I do -
let lineWidth = self.lineWidth * 20 / pow(zoomScale, 0.6)
context.setLineWidth(lineWidth)
which gives me a visually pleasing line thickness for all zoom levels.

Related

show masking on object which is between camera and wall using RealityKit

I made a video for generating a floor plan in which I need to capture the wall and floor together at a certain position if a user is too near to the wall or if any object come between the camera and wall/floor then need to show Too Close mask on that object something like display in this video.
I try to use rycast in session(_ session: ARSession, didUpdate frame: ARFrame) method but I am very new in AR and not know which method we need to use.
func session(_ session: ARSession, didUpdate frame: ARFrame) {
guard let query = self.arView?.makeRaycastQuery(from: self.arView?.center ?? CGPoint.zero,
allowing: .estimatedPlane,
alignment: .any)
else { return }
guard let raycastResult = self.arView?.session.raycast(query).first
else { return }
let currentPositionOfCamera = raycastResult.worldTransform.getPosition()
if currentPositionOfCamera != .zero {
let distanceFromCamera = frame.camera.transform.getPosition().distanceFrom(position: currentPositionOfCamera)
print("Distance from raycast:",distanceFromCamera)
if (distance < 0.5) {
print("Too Close")
}
}
}
I am just learning ARKit and RealityKit as well, but wouldn't your code be:
let currentPositionOfCamera = self.arView.cameraTransform.translation
if currentPositionOfCamera != .zero {
// distance is defined in simd as the distance between 2 points
let distanceFromCamera = distance(raycastResult.worldTransform.position, currentPositionOfCamera)
print("Distance from raycast:",distanceFromCamera)
if (distanceFromCamera < 0.5) {
print("Too Close")
let rayDirection = normalize(raycastResult.worldTransform.position - self.arView.cameraTransform.translation)
// This pulls the text back toward the camera from the plane
let textPositionInWorldCoordinates = result.worldTransform.position - (rayDirection * 0.1)
let textEntity = self.model(for: classification)
// This scales the text so it is of a consistent size
textEntity.scale = .one * raycastDistance
var textPositionWithCameraOrientation = self.arView.cameraTransform
textPositionWithCameraOrientation.translation = textPositionInWorldCoordinates
// self.textAnchor is defined somewhere in the class as an optional
self.textAnchor = AnchorEntity(world: textPositionWithCameraOrientation.matrix)
textAnchor.addChild(textEntity)
self.arView.scene.addAnchor(textAnchor)
} else {
guard let textAnchor = self.textAnchor else { return }
self.removeAnchor(textAnchor)
}
}
// Creates a text ModelEntity
func tooCloseModel() -> ModelEntity {
let lineHeight: CGFloat = 0.05
let font = MeshResource.Font.systemFont(ofSize: lineHeight)
let textMesh = MeshResource.generateText("Too Close", extrusionDepth: Float(lineHeight * 0.1), font: font)
let textMaterial = SimpleMaterial(color: classification.color, isMetallic: true)
let model = ModelEntity(mesh: textMesh, materials: [textMaterial])
// Center the text
model.position.x -= model.visualBounds(relativeTo: nil).extents.x / 2
return model
}
This code is adapted from Apple's Visualizing Scene Semantics.

How do I render CALayer sublayers in the correct position

I am trying to get an image drawn of a CALayer containing a number of sublayers positioned at specific points, but at the moment it does not honour the zPosition of the sublayers when I use CALayer.render(in ctx:). It works fine on screen but when rendering to PDF it seems to render them in the order they were created.
These sublayers that are positioned(x, y, angle) on the drawing layer.
One solution seems to be to override the render(in ctx:) method on the drawing layer, which seems to work except the rendering of the sublayers is in the incorrect position. They are all in the bottom left corner (0,0) and not rotated correctly.
override func render(in ctx: CGContext) {
if let layers:[CALayer] = self.sublayers {
let orderedLayers = layers.sorted(by: {
$0.zPosition < $1.zPosition
})
for v in orderedLayers {
v.render(in: ctx)
}
}
}
If I don't override this method then they are positioned correctly but just in the wrong zPosition - i.e. ones that should be at the bottom (zPosition-0) are at the top.
What am I missing here ? It seems I need to position the sublayers correctly somehow in the render(incts:) function?
How do I do this ? These sublayers have already been positioned on screen and all I am trying to do is generate an image of the drawing. This is done using the following function.
func createPdfData()->Data?{
DebugLog("")
let scale: CGFloat = 1
let mWidth = drawingLayer.frame.width * scale
let mHeight = drawingLayer.frame.height * scale
var cgRect = CGRect(x: 0, y: 0, width: mWidth, height: mHeight)
let documentInfo = [kCGPDFContextCreator as String:"MakeSpace(www.xxxx.com)",
kCGPDFContextTitle as String:"Layout Image",
kCGPDFContextAuthor as String:GlobalVars.shared.appUser?.username ?? "",
kCGPDFContextSubject as String:self.level?.imageCode ?? "",
kCGPDFContextKeywords as String:"XXXX, Layout"]
let data = NSMutableData()
guard let pdfData = CGDataConsumer(data: data),
let ctx = CGContext.init(consumer: pdfData, mediaBox: &cgRect, documentInfo as CFDictionary) else {
return nil}
ctx.beginPDFPage(nil)
ctx.saveGState()
ctx.scaleBy(x: scale, y: scale)
self.drawingLayer.render(in: ctx)
ctx.restoreGState()
ctx.endPDFPage()
ctx.closePDF()
return data as Data
}
This is what I ended up doing - and it seems to work.
class ZOrderDrawingLayer: CALayer {
override func render(in ctx: CGContext) {
if let layers:[CALayer] = self.sublayers {
let orderedLayers = layers.sorted(by: {
$0.zPosition < $1.zPosition
})
for v in orderedLayers {
ctx.saveGState()
// Translate and rotate the context using the sublayers
// size, position and transform (angle)
let w = v.bounds.width/2
let ww = w*w
let h = v.bounds.height/2
let hh = h*h
let c = sqrt(ww + hh)
let theta = asin(h/c)
let angle = atan2(v.transform.m12, v.transform.m11)
let x = c * cos(theta+angle)
let y = c * sin(theta+angle)
ctx.translateBy(x: v.position.x-x, y: v.position.y-y)
ctx.rotate(by: angle)
v.render(in: ctx)
ctx.restoreGState()
}
}
}
}

Place node in front of the camera and rotate

I want to show a pointer when a user double taps on a sceneView. For this, I want to use SCNTorus because at some angle it is a circle.
let geometry = SCNTorus(ringRadius: 0.01, pipeRadius: 0.001)
let node = SCNNode(geometry: geometry)
I already get a vector from hitTest that I use as a position for the node
private func getVector(for point: CGPoint) -> SCNVector3? {
guard let hitTest = self.sceneView.hitTest(point, types: .featurePoint).first else {
return nil
}
let transform = SCNMatrix4.init(hitTest.worldTransform)
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
return vector
}
How can I transform it to always appear as a circle when the node added.
For vertical device position rotating by .pi/2 along X-axis works great but it breaks when devices move in a space, so I can not hardcode the angle
node.eulerAngles.x = .pi / 2
sceneView.scene.rootNode.addChildNode(node)
I believe I need to sceneView.pointOfView somehow to apply the transformation. But I stuck here.
Here is the full code
#objc private func didDoubleTap(_ sender: UITapGestureRecognizer) {
let point = sender.location(in: sceneView)
guard let vector = getVector(for: point) else { return }
guard let pov = sceneView.pointOfView else { return }
guard let camera = sceneView.session.currentFrame?.camera else { return }
let geometry = SCNTorus(ringRadius: 0.01, pipeRadius: 0.001)
let node = SCNNode(geometry: geometry)
node.position = vector
node.eulerAngles.x = .pi / 2
sceneView.scene.rootNode.addChildNode(node)
animatePointingNode(node)
}
I was able to solve the issue by setting eulerAngles.y to be equal to camera's Y and adjusting camera's X by .pi/2
guard let camera = sceneView.session.currentFrame?.camera else { return }
pointerNode.position = vector
pointerNode.eulerAngles.x = camera.eulerAngles.x + .pi / 2
pointerNode.eulerAngles.y = camera.eulerAngles.y

How to draw a line between two points in SceneKit?

If I have two points in SceneKit (e.g. (1,2,3) and (-1,-1,-1)). How do I draw a line between the two?
I see that there is a SCNBox object I may be able to use, but that only allows me to specify the center (e.g. via simdPosition). The other ways to modify it are the transform (which I don't know how to use), or the Euler angles (which I'm not sure how to calculate which ones I need to use).
You can draw a line between two points using the following approach:
import SceneKit
extension SCNGeometry {
class func line(vector1: SCNVector3,
vector2: SCNVector3) -> SCNGeometry {
let sources = SCNGeometrySource(vertices: [vector1,
vector2])
let index: [Int32] = [0,1]
let elements = SCNGeometryElement(indices: index,
primitiveType: .line)
return SCNGeometry(sources: [sources],
elements: [elements])
}
}
...and then feed it to addLine function in ViewController:
class ViewController: UIViewController {
// Some code...
func addLine(start: SCNVector3, end: SCNVector3) {
let lineGeo = SCNGeometry.line(vector1: start,
vector2: end)
let lineNode = SCNNode(geometry: lineGeo)
sceneView.scene.rootNode.addChildNode(lineNode)
}
}
As we all know line's width can't be changed (cause there's no property to do it), so you can use cylinder primitive geometry instead of a line:
extension SCNGeometry {
class func cylinderLine(from: SCNVector3,
to: SCNVector3,
segments: Int) -> SCNNode {
let x1 = from.x
let x2 = to.x
let y1 = from.y
let y2 = to.y
let z1 = from.z
let z2 = to.z
let distance = sqrtf( (x2-x1) * (x2-x1) +
(y2-y1) * (y2-y1) +
(z2-z1) * (z2-z1) )
let cylinder = SCNCylinder(radius: 0.005,
height: CGFloat(distance))
cylinder.radialSegmentCount = segments
cylinder.firstMaterial?.diffuse.contents = UIColor.green
let lineNode = SCNNode(geometry: cylinder)
lineNode.position = SCNVector3(x: (from.x + to.x) / 2,
y: (from.y + to.y) / 2,
z: (from.z + to.z) / 2)
lineNode.eulerAngles = SCNVector3(Float.pi / 2,
acos((to.z-from.z)/distance),
atan2((to.y-from.y),(to.x-from.x)))
return lineNode
}
}
...then feed it the same way to ViewController:
class ViewController: UIViewController {
// Some code...
func addLine(start: SCNVector3, end: SCNVector3) {
let cylinderLineNode = SCNGeometry.cylinderLine(from: start,
to: end,
segments: 3)
sceneView.scene.rootNode.addChildNode(cylinderLineNode)
}
}
First you'll need to calculate the heading and pitch between the two points. Full post is here and this answer explains how to do it between any arbitrary two points.
Once you have your two angles, if you attempt to use the Euler angles on an SCNBox, you'll notice that when you only modify the pitch (eulerAngles.x), or only modify the heading (eulerAngles.y), everything works fine. However, the moment you try to modify both, you'll run into issues. One solution is to wrap one node inside another.
This seemed like such a useful suggestion, that I create a handy wrapper node that should handle rotations upon all 3 axes:
import Foundation
import SceneKit
struct HeadingPitchBank {
let heading: Float
let pitch: Float
let bank: Float
/// returns the heading and pitch (bank is 0) represented by the vector
static func from(vector: simd_float3) -> HeadingPitchBank {
let heading = atan2f(vector.x, vector.z)
let pitch = atan2f(sqrt(vector.x*vector.x + vector.z*vector.z), vector.y) - Float.pi / 2.0
return HeadingPitchBank(heading: heading, pitch: pitch, bank: 0)
}
}
class HeadingPitchBankWrapper: SCNNode {
init(wrappedNode: SCNNode) {
headingNode = SCNNode()
pitchNode = SCNNode()
bankNode = SCNNode()
_wrappedNode = wrappedNode
super.init()
addChildNode(headingNode)
headingNode.addChildNode(pitchNode)
pitchNode.addChildNode(bankNode)
bankNode.addChildNode(wrappedNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var heading: Float {
get {
return headingNode.eulerAngles.y
}
set {
headingNode.eulerAngles.y = newValue
}
}
var pitch: Float {
get {
return pitchNode.eulerAngles.x
}
set {
pitchNode.eulerAngles.x = newValue
}
}
var bank: Float {
get {
return bankNode.eulerAngles.z
}
set {
bankNode.eulerAngles.z = newValue
}
}
var headingPitchBank: HeadingPitchBank {
get {
return HeadingPitchBank(heading: heading, pitch: pitch, bank: bank)
}
set {
heading = newValue.heading
pitch = newValue.pitch
bank = newValue.bank
}
}
var wrappedNode: SCNNode {
return _wrappedNode
}
private var headingNode: SCNNode
private var pitchNode: SCNNode
private var bankNode: SCNNode
private var _wrappedNode: SCNNode
}
You could then use this to easily draw a line between two points:
func createLine(start: simd_float3 = simd_float3(), end: simd_float3, color: UIColor, opacity: CGFloat? = nil, radius: CGFloat = 0.005) -> SCNNode {
let length = CGFloat(simd_length(end-start))
let box = SCNNode(geometry: SCNBox(width: radius, height: radius, length: length, chamferRadius: 0))
box.geometry!.firstMaterial!.diffuse.contents = color
let wrapper = HeadingPitchBankWrapper(wrappedNode: box)
wrapper.headingPitchBank = HeadingPitchBank.from(vector: end - start)
wrapper.simdPosition = midpoint(start, end)
if let opacity = opacity {
wrapper.opacity = opacity
}
return wrapper
}
Just build a custom geometry using SCNGeometryPrimitiveType.line:
let vertices: [SCNVector3] = [
SCNVector3(1, 2, 3),
SCNVector3(-1, -1, -1)
]
let linesGeometry = SCNGeometry(
sources: [
SCNGeometrySource(vertices: vertices)
],
elements: [
SCNGeometryElement(
indices: [Int32]([0, 1]),
primitiveType: .line
)
]
)
let line = SCNNode(geometry: linesGeometry)
scene.rootNode.addChildNode(line)

How to display large amount of GMSPolylines without maxing out CPU usage?

I'm making an app that displays bus routes using the NextBus API and Google Maps. However, I'm having an issue with CPU usage that I think is being caused by the amount of GMSPolylines on the map. The route is displayed by an array of polylines made up of the points given by NextBus for a given route. When the polylines are added to the map and the GMSCamera is overviewing the entire route, the CPU on the simulator (iPhone X) maxes out at 100%. When zoomed in on a particular section of the route, however, the CPU usage goes down to ~2%.
Map Screenshot: https://i.imgur.com/jLmN26e.png
Performance: https://i.imgur.com/nUbIv5w.png
The NextBus API returns route information including the route of a specific bus path. Here's an small example of the data that I'm working with:
Route: {
"path": [Path]
}
Path: {
"points:" [Coordinate]
}
Coordinate: {
"lat": Float,
"lon": Float
}
And here's my method that creates the polylines from the data. All in all there are on average ~700 coordinates spread across ~28 polylines (each path object) for a route. Keep in mind I'm not displaying multiple routes on one page, I'm only displaying one at a time.
func buildRoute(routePath: [Path?]) -> [GMSPolyline] {
var polylines: [GMSPolyline] = []
for path in routePath {
let path = GMSMutablePath()
guard let coords = path?.points else {continue}
for coordinate in coords {
// Safely unwrap latitude strings and convert them to doubles.
guard let latStr = coordinate?.lat,
let lonStr = coordinate?.lon else {
continue
}
guard let latOne = Double(latStr),
let lonOne = Double(lonStr) else {
continue
}
// Create location coordinates.
let pointCoordinatie = CLLocationCoordinate2D(latitude: latOne, longitude: lonOne)
path.add(pointCoordinatie)
}
let line = GMSPolyline(path: path)
line.strokeWidth = 6
line.strokeColor = UIColor(red: 0/255, green: 104/255, blue: 139/255, alpha: 1.0)
polylines.append(line)
}
return polylines
}
Finally here is my method that adds the polylines to the map:
fileprivate func buildRoute(routeConfig: RouteConfig?) {
if let points = routeConfig?.route?.path {
let polylines = RouteBuiler.shared.buildRoute(routePath: points)
DispatchQueue.main.async {
// Remove polylines from map if there are any.
for line in self.currentRoute {
line.map = nil
}
// Set new current route and add it to the map.
self.currentRoute = polylines
for line in self.currentRoute {
line.map = self.mapView
}
}
}
}
Is there a problem with how I'm constructing the polylines? Or are there simply too many coordinates?
I ran into this exact problem. It is quite an odd bug -- when you go over a certain threshold of polylines, the CPU suddenly pegs to 100%.
I discovered that GMSPolygon does not have this problem. So I switched over all of GMSPolyline to GMSPolygon.
To get the correct stroke width, I am using the following code to create a polygon that traces the outline of a polyline at a given stroke width. My calculation requires the LASwift linear algebra library.
https://github.com/AlexanderTar/LASwift
import CoreLocation
import LASwift
import GoogleMaps
struct Segment {
let from: CLLocationCoordinate2D
let to: CLLocationCoordinate2D
}
enum RightLeft {
case right, left
}
// Offset the given path to the left or right by the given distance
func offsetPath(rightLeft: RightLeft, path: [CLLocationCoordinate2D], offset: Double) -> [CLLocationCoordinate2D] {
var offsetPoints = [CLLocationCoordinate2D]()
var prevSegment: Segment!
for i in 0..<path.count {
// Test if this is the last point
if i == path.count-1 {
if let to = prevSegment?.to {
offsetPoints.append(to)
}
continue
}
let from = path[i]
let to = path[i+1]
// Skip duplicate points
if from.latitude == to.latitude && from.longitude == to.longitude {
continue
}
// Calculate the miter corner for the offset point
let segmentAngle = -atan2(to.latitude - from.latitude, to.longitude - from.longitude)
let sinA = sin(segmentAngle)
let cosA = cos(segmentAngle)
let rotate =
Matrix([[cosA, -sinA, 0.0],
[sinA, cosA, 0.0],
[0.0, 0.0, 1.0]])
let translate =
Matrix([[1.0, 0.0, 0.0 ],
[0.0, 1.0, rightLeft == .left ? offset : -offset ],
[0.0, 0.0, 1.0]])
let mat = inv(rotate) * translate * rotate
let fromOff = mat * Matrix([[from.x], [from.y], [1.0]])
let toOff = mat * Matrix([[to.x], [to.y], [1.0]])
let offsetSegment = Segment(
from: CLLocationCoordinate2D(latitude: fromOff[1,0], longitude: fromOff[0,0]),
to: CLLocationCoordinate2D(latitude: toOff[1,0], longitude: toOff[0,0]))
if prevSegment == nil {
prevSegment = offsetSegment
offsetPoints.append(offsetSegment.from)
continue
}
// Calculate line intersection
guard let intersection = getLineIntersection(line0: prevSegment, line1: offsetSegment, segment: false) else {
prevSegment = offsetSegment
continue
}
prevSegment = offsetSegment
offsetPoints.append(intersection)
}
return offsetPoints
}
// Returns the intersection point if the line segments intersect, otherwise nil
func getLineIntersection(line0: Segment, line1: Segment, segment: Bool) -> CLLocationCoordinate2D? {
return getLineIntersection(p0: line0.from, p1: line0.to, p2: line1.from, p3: line1.to, segment: segment)
}
// https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
// Returns the intersection point if the line segments intersect, otherwise nil
func getLineIntersection(p0: CLLocationCoordinate2D, p1: CLLocationCoordinate2D, p2: CLLocationCoordinate2D, p3: CLLocationCoordinate2D, segment: Bool) -> CLLocationCoordinate2D? {
let s1x = p1.longitude - p0.longitude
let s1y = p1.latitude - p0.latitude
let s2x = p3.longitude - p2.longitude
let s2y = p3.latitude - p2.latitude
let numerator = (s2x * (p0.latitude - p2.latitude) - s2y * (p0.longitude - p2.longitude))
let denominator = (s1x * s2y - s2x * s1y)
if denominator == 0.0 {
return nil
}
let t = numerator / denominator
if segment {
let s = (s1y * (p0.longitude - p2.longitude) + s1x * (p0.latitude - p2.latitude)) / (s1x * s2y - s2x * s1y)
guard (s >= 0 && s <= 1 && t >= 0 && t <= 1) else {
return nil
}
}
return CLLocationCoordinate2D(latitude: p0.latitude + (t * s1y), longitude: p0.longitude + (t * s1x))
}
// The path from NextBus
let path: CLLocationCoordinate2D = pathFromNextBus()
// The desired width of the polyline
let strokeWidth: Double = desiredPolylineWidth()
let polygon: GMSPolygon
do {
let polygonPath = GMSMutablePath()
let w = strokeWidth / 2.0
for point in offsetPath(rightLeft: .left, path: route.offsetPath, offset: w) {
polygonPath.add(CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude))
}
for point in offsetPath(rightLeft: .right, path: route.offsetPath, offset: w).reversed() {
polygonPath.add(CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude))
}
polygon = GMSPolygon(path: polygonPath)
polygon.strokeWidth = 0.0
}