2

To avoid being an XY problem, I will talk about some background first.

I am writing the game Planarity as an iOS app. The game starts off with a graph and the player has to move the vertices of the graph so that none of the edges intersect another edge.

I want to color code the edges so that edges that intersect another edge will be in red and those that does not intersect other edges will be in green.

I used a custom UIView called GraphView to display the graph. The GraphView contains subviews of type NodeView, which are used to represent the vertices. In the draw(_:) method of GraphView, I draw the edges between the vertices:

override func draw(_ rect: CGRect) {
    // "graph" is the model. It stores where all the vertices are and which vertex is connected to which
    if graph == nil { return }

    // intersectionDict is of type [Connection: Bool]. It stores whether a Connection intersects with another connection
    // value is "true" for a connection that should be red
    // and "false" for a connection that should be green
    let intersectionDict = graph.checkIntersections()

    for connection in graph.connections {
        let path = UIBezierPath()
        path.lineWidth = 2
        path.move(to: CGPoint(x: connection.node1.x, y: connection.node1.y))
        path.addLine(to: CGPoint(x: connection.node2.x, y: connection.node2.y))
        // assume this is always true, because it is irrelevant
        if UserSettings.colorCodeConnections {
            if intersectionDict[connection] ?? false {
                UIColor.red.setStroke()
            } else {
                UIColor.green.darker().setStroke()
            }
        } else {
            UIColor.black.setStroke()
        }
        path.stroke()
    }
}

I set up a CADisplayLink to call setNeedsDisplay every frame so that the color of the connections will respond instantly to a user's dragging of the vertices.

When I increase the number of vertices and edges of the graph, the app becomes very laggy. I suppose this is because draw can't return in 1/60 of second (one frame).

I used Instruments to find out that indeed draw is doing a lot of work:

enter image description here

I think I need to make checkIntersection run more quickly.

This is checkIntersections:

func checkIntersections() -> [Connection: Bool] {
    var dict = [Connection: Bool]()
    var tempConnections = connections // tempConnections is a Set<Connection>
    while true {
        // find the next connection to check
        // we subtract the dictionary's keys here because we don't need to check them (they are already checked)
        if let connectionToCheck = tempConnections.subtracting(dict.keys).first {
            // get all the connections that connectionToCheck intersects with
            let intersections = Set(tempConnections.filter { connectionToCheck.intersects(with: $0) })
            if intersections.isEmpty {
                dict[connectionToCheck] = false
                tempConnections.remove(connectionToCheck)
            } else {
                dict[connectionToCheck] = true
                for connection in intersections {
                    dict[connection] = true
                }
            }
        } else {
            break
        }
    }
    return dict
}

As you can see, I knew that this would take a lot of time when I was writing this algorithm, so I already tried to check as few connections as possible. But since this is so slow, I wonder if I can check even fewer connections. It's ok if this is already the fewest number of checks.

How can I check the fewest connections possible?

EDIT:

Martin R suggested that I should use a nested for loop:

func checkIntersections() -> [Connection: Bool] {
    var dict = [Connection: Bool]()

    let arrConnections = Array(connections)
    for i in 0..<arrConnections.count {
        for j in (i+1)..<arrConnections.count {
            if arrConnections[i].intersects(with: arrConnections[j]) {
                dict[arrConnections[i]] = true
                dict[arrConnections[j]] = true
            }
        }
    }
    return dict
}

From Instruments, I can see that the "Weight" of the checkIntersections call is about 50%, which is roughly the same as before, when I used sets.

As for the intersects(with:) method, I adapted a solution from another SO answer (that I now can't find), it looks like this:

func intersects(with connection: Connection) -> Bool {
    let p1 = self.node1
    let p2 = self.node2
    let p3 = connection.node1
    let p4 = connection.node2
    let d = (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x)
    if d == 0 {
        return false
    }

    // if a line starts at where another ends, they don't intersect
    // samePointAs just checks whether the two nodes have the same coordinates
    if p2.samePointAs(p3) || p4.samePointAs(p1) || p2.samePointAs(p4) || p1.samePointAs(p3){
        return false
    }

    let u = ((p3.x - p1.x)*(p4.y - p3.y) - (p3.y - p1.y)*(p4.x - p3.x))/d
    let v = ((p3.x - p1.x)*(p2.y - p1.y) - (p3.y - p1.y)*(p2.x - p1.x))/d
    if !(0.0...1.0).contains(u) || !(0.0...1.0).contains(v) {
        return false
    }
    return true
}

The original answer finds the intersection point of two line segments, not just checking whether they intersect. Maybe just checking for intersection can be more easily done than this?

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Have you tried to store all connections in an array and then use a simple nested loop instead of all those dictionary/set/filter operations? – For more efficiency you probably have to take the geometry into account and save the connections in some way that possible intersection candidates are found quickly. – Martin R May 07 '18 at 08:14
  • @MartinR I was trying to reduce the number of checks. Surely using a nested loop will cause more checks and it will be slower, right? – Sweeper May 07 '18 at 08:17
  • In any case you *somehow* have to check each connection against each other connection (unless you can filter out candidates based on their coordinates). That's why a nested loop (with indices (0 <= i < j < connections.count) might be faster than repeatedly modifying collections. – Martin R May 07 '18 at 08:21
  • @MartinR See the edit. It doesn't seem to have improved much. Did I understand you correctly? Or did I look at the wrong number in Instruments? – Sweeper May 07 '18 at 08:41
  • That's what I meant (only that I would return a `Set` instead of a dictionary, or even store the information in a boolean flag property in the Connection itself). – But as I said, a substantially better solution has to take the geometry into account. – Martin R May 07 '18 at 08:45
  • @MartinR `save the connections in some way that possible intersection candidates are found quickly.` <-- but the user can drag the vertices and their positions will change. This means that the geometry will change all the time, right? There couldn't be _one_ way to store them to allow intersection to be easily found, could there? I'm sorry but to be honest I'm quite bad at maths... – Sweeper May 07 '18 at 08:49
  • I doubt that there is a “simple” solution, and this looks more like a problem on geometric graph theory (for which I am not an expert) than a question about Swift. – A similar problem is whether a polygon is self-intersecting. As you can see here https://math.stackexchange.com/q/80798 and here https://stackoverflow.com/q/4876065, this already is not trivial. – One (final) thought: How did you implement `intersects(with:)`? Perhaps that can be improved a bit. – Martin R May 07 '18 at 08:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/170522/discussion-between-sweeper-and-martin-r). – Sweeper May 07 '18 at 09:05

0 Answers0