Skip to content

Commit

Permalink
Async projection (#32)
Browse files Browse the repository at this point in the history
* Offload projection to a detached task to not block main actor

* Attempt to improve this

* Refactor into `projectInParallel` helper

* Update packages
  • Loading branch information
nighthawk authored Oct 29, 2024
1 parent 699e5d9 commit 3985921
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 34 deletions.
30 changes: 24 additions & 6 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,44 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/maparoni/geojsonkit.git",
"state" : {
"revision" : "8f1e5094a8ab8cbb020074cd4848a5804c17bab0",
"version" : "0.5.2"
"revision" : "a5aabe3a1edddbcf14b49b3fa5be1ddd48dff7c8",
"version" : "0.6.0"
}
},
{
"identity" : "geojsonkit-turf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/maparoni/geojsonkit-turf",
"state" : {
"revision" : "21452ac7ee183930325e786575d552129c21b165",
"version" : "0.2.0"
"revision" : "99c542f4c3c105caa9567c61fd8036e94e07df05",
"version" : "0.4.1"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-algorithms.git",
"state" : {
"revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42",
"version" : "1.2.0"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.2.3"
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics.git",
"state" : {
"revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
"version" : "1.0.2"
}
}
],
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ let package = Package(
.package(url: "https://github.com/maparoni/geojsonkit.git", from: "0.5.0"),
.package(url: "https://github.com/maparoni/geojsonkit-turf", from: "0.1.0"),
// .package(name: "geojsonkit-turf", path: "../GeoJSONKit-Turf"),
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
],
targets: [
.target(
Expand All @@ -38,6 +39,7 @@ let package = Package(
name: "GeoDrawer",
dependencies: [
"GeoProjector",
.product(name: "Algorithms", package: "swift-algorithms"),
]),
]
)
35 changes: 35 additions & 0 deletions Sources/GeoDrawer/GeoDrawer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CoreGraphics
#endif

import GeoJSONKit
import Algorithms

@_exported import GeoProjector

Expand Down Expand Up @@ -227,6 +228,40 @@ extension GeoDrawer {
return .circle(point, radius: radius, fill: fill, stroke: stroke, strokeWidth: strokeWidth)
}
}

struct OffsettedElement<Element: Equatable>: Comparable, Equatable {
let offset: Int
let element: Element

static func == (lhs: OffsettedElement, rhs: OffsettedElement) -> Bool {
lhs.offset == rhs.offset && lhs.element == rhs.element
}

static func < (lhs: OffsettedElement, rhs: OffsettedElement) -> Bool {
lhs.offset < rhs.offset
}
}

func projectInParallel(_ contents: [Content]) async throws -> [ProjectedContent] {
try await withThrowingTaskGroup(of: [OffsettedElement<ProjectedContent>].self) { group in
let chunks = Array(contents.enumerated()).chunks(ofCount: 25)
for chunk in chunks {
let added = group.addTaskUnlessCancelled {
await Task {
return chunk.compactMap { input in
project(input.element).flatMap { OffsettedElement(offset: input.offset, element: $0) }
}
}.value
}
if !added {
throw CancellationError()
}
}

let unsorted = try await group.reduce(into: []) { $0.append(contentsOf: $1) }
return unsorted.sorted(by: <).map(\.element)
}
}
}

// MARK: - Line helper
Expand Down
60 changes: 57 additions & 3 deletions Sources/GeoDrawer/GeoMap+AppKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,32 @@ import GeoJSONKit

public class GeoMapView: NSView {
public var contents: [GeoDrawer.Content] = [] {
didSet { setNeedsDisplay(bounds) }
didSet {
invalidateProjectedContents()
setNeedsDisplay(bounds)
}
}

public var projection: Projection = Projections.Equirectangular() {
didSet {
_drawer = nil
invalidateProjectedContents()
setNeedsDisplay(bounds)
}
}

public var zoomTo: GeoJSON.BoundingBox? = nil {
didSet {
_drawer = nil
invalidateProjectedContents()
setNeedsDisplay(bounds)
}
}

public var insets: GeoProjector.EdgeInsets = .zero {
didSet {
_drawer = nil
invalidateProjectedContents()
setNeedsDisplay(bounds)
}
}
Expand All @@ -61,6 +67,7 @@ public class GeoMapView: NSView {
public override var frame: NSRect {
didSet {
_drawer = nil
invalidateProjectedContents()
setNeedsDisplay(bounds)
}
}
Expand All @@ -82,20 +89,67 @@ public class GeoMapView: NSView {
}

public override func draw(_ rect: NSRect) {
super.draw(rect)
// Don't draw if we're busy as this will flicker weirdly
let projected: [GeoDrawer.ProjectedContent]
switch projectProgress {
case .busy(_, .some(let previous)):
projected = previous
case .busy(_, .none), .idle:
return // Don't update drawing; will get called again instead when finished
case .finished(let finished):
projected = finished
}

super.draw(rect)

// Get the current graphics context and cast it to a CGContext
let context = NSGraphicsContext.current!.cgContext

// Use Core Graphics functions to draw the content of your view
drawer.draw(
contents,
projected,
mapBackground: mapBackground.cgColor,
mapOutline: mapOutline.cgColor,
mapBackdrop: mapBackdrop.cgColor,
in: context
)
}

// MARK: - Performance

enum ProjectionProgress {
case finished([GeoDrawer.ProjectedContent])
case busy(Task<Void, Never>, previously: [GeoDrawer.ProjectedContent]?)
case idle
}

private var projectProgress = ProjectionProgress.idle

private func invalidateProjectedContents() {
let previous: [GeoDrawer.ProjectedContent]?
switch projectProgress {
case .finished(let projected):
previous = projected
case .busy(let task, let previously):
task.cancel()
previous = previously
case .idle:
previous = nil
}

projectProgress = .busy(Task(priority: .high) { [weak self] in
guard let self else { return }
do {
let projected = try await drawer.projectInParallel(contents)
await MainActor.run {
self.projectProgress = .finished(projected)
self.setNeedsDisplay(self.bounds)
}
} catch {
assert(error is CancellationError)
}
}, previously: previous)
}
}

@available(macOS 10.15, *)
Expand Down
90 changes: 65 additions & 25 deletions Sources/GeoDrawer/GeoMap+UIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,57 @@ import GeoJSONKit
public class GeoMapView: UIView {
public var contents: [GeoDrawer.Content] = [] {
didSet {
_projected = nil
setNeedsDisplay(bounds)
if contents == oldValue { return }
invalidateProjectedContents()
setNeedsDisplay()
}
}

public var projection: Projection = Projections.Equirectangular() {
didSet {
_drawer = nil
_projected = nil
setNeedsDisplay(bounds)
invalidateProjectedContents()
setNeedsDisplay()
}
}

public var zoomTo: GeoJSON.BoundingBox? = nil {
didSet {
if zoomTo == oldValue { return }
_drawer = nil
_projected = nil
setNeedsDisplay(bounds)
invalidateProjectedContents()
setNeedsDisplay()
}
}

public var insets: GeoProjector.EdgeInsets = .zero {
didSet {
if insets == oldValue { return }
_drawer = nil
_projected = nil
setNeedsDisplay(bounds)
invalidateProjectedContents()
setNeedsDisplay()
}
}

public var mapBackground: UIColor = .systemTeal {
didSet {
setNeedsDisplay(bounds)
setNeedsDisplay()
}
}

public var mapOutline: UIColor = .black {
didSet {
setNeedsDisplay(bounds)
setNeedsDisplay()
}
}


public override var frame: CGRect {
didSet {
if frame == oldValue { return }
_drawer = nil
_projected = nil
setNeedsDisplay(bounds)
}
}


private var _projected: [GeoDrawer.ProjectedContent]!
private var projected: [GeoDrawer.ProjectedContent] {
if let _projected {
return _projected
} else {
let projected = contents.compactMap(drawer.project(_:))
_projected = projected
return projected
invalidateProjectedContents()
setNeedsDisplay()
}
}

Expand All @@ -96,7 +88,6 @@ public class GeoMapView: UIView {
}

public override func draw(_ rect: CGRect) {
super.draw(rect)

// Get the current graphics context and cast it to a CGContext
let context = UIGraphicsGetCurrentContext()!
Expand All @@ -110,6 +101,19 @@ public class GeoMapView: UIView {
background = .white
}

// Don't draw if we're busy as this will flicker weirdly
let projected: [GeoDrawer.ProjectedContent]
switch projectProgress {
case .busy(_, .some(let previous)):
projected = previous
case .busy(_, .none), .idle:
return // Don't update drawing; will get called again instead when finished
case .finished(let finished):
projected = finished
}

super.draw(rect)

// Use Core Graphics functions to draw the content of your view
drawer.draw(
projected,
Expand All @@ -121,6 +125,42 @@ public class GeoMapView: UIView {

context.flush()
}

// MARK: - Performance

enum ProjectionProgress {
case finished([GeoDrawer.ProjectedContent])
case busy(Task<Void, Never>, previously: [GeoDrawer.ProjectedContent]?)
case idle
}

private var projectProgress = ProjectionProgress.idle

private func invalidateProjectedContents() {
let previous: [GeoDrawer.ProjectedContent]?
switch projectProgress {
case .finished(let projected):
previous = projected
case .busy(let task, let previously):
task.cancel()
previous = previously
case .idle:
previous = nil
}

projectProgress = .busy(Task.detached(priority: .high) { [weak self] in
guard let self else { return }
do {
let projected = try await drawer.projectInParallel(contents)
await MainActor.run {
self.projectProgress = .finished(projected)
self.setNeedsDisplay(self.bounds)
}
} catch {
assert(error is CancellationError)
}
}, previously: previous)
}
}

@available(iOS 13.0, visionOS 1.0, *)
Expand Down

0 comments on commit 3985921

Please sign in to comment.