Skip to content

Commit

Permalink
Improve performance by projecting contents just once and caching result
Browse files Browse the repository at this point in the history
  • Loading branch information
nighthawk committed Oct 24, 2024
1 parent 963b49f commit 699e5d9
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 84 deletions.
174 changes: 95 additions & 79 deletions Sources/GeoDrawer/GeoDrawer+CoreGraphics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,103 +20,111 @@ extension GeoDrawer {

/// Draws the line into the current context
public func draw(_ line: GeoJSON.LineString, strokeColor: CGColor, strokeWidth: Double = 2, in context: CGContext) {
for points in convertLine(line.positions) {

let path = CGMutablePath()
path.move(to: points[0].cgPoint)
for point in points[1...] {
path.addLine(to: point.cgPoint)
}

context.setStrokeColor(strokeColor)

context.setLineWidth(strokeWidth)
context.setLineCap(.round)
context.setLineJoin(.round)

context.addPath(path)
context.strokePath()
for line in project(line) {
draw(line, strokeColor: strokeColor, strokeWidth: strokeWidth, in: context)
}
}

public func draw(_ polygon: GeoJSON.Polygon, fillColor: CGColor? = nil, strokeColor: CGColor? = nil, strokeWidth: Double = 2, frame: CGRect, in context: CGContext) {
// In some projections such as Azimuthal, we might need to colour a cut-out
// rather than the projected polygon.
let invert: Bool = invertCheck?(polygon) ?? false
for polygon in project(polygon) {
draw(polygon, fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, frame: frame, in: context)
}
}

func drawCircle(_ position: GeoJSON.Position, radius: CGFloat, fillColor: CGColor, strokeColor: CGColor? = nil, strokeWidth: Double = 2, in context: CGContext) {
guard let center = converter(position)?.0 else { return }
drawCircle(center, radius: radius, fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, in: context)
}

func draw(_ projectedLine: ProjectedLineString, strokeColor: CGColor, strokeWidth: Double, in context: CGContext) {
let points = projectedLine.points
let path = CGMutablePath()
path.move(to: points[0].cgPoint)
for point in points[1...] {
path.addLine(to: point.cgPoint)
}

for points in convertLine(polygon.exterior.positions) {

let path = CGMutablePath()
path.move(to: points[0].cgPoint)
for point in points[1...] {
path.addLine(to: point.cgPoint)
}
context.setStrokeColor(strokeColor)

context.setLineWidth(strokeWidth)
context.setLineCap(.round)
context.setLineJoin(.round)

context.addPath(path)
context.strokePath()
}

func draw(_ polygon: ProjectedPolygon, fillColor: CGColor?, strokeColor: CGColor?, strokeWidth: Double, frame: CGRect, in context: CGContext) {
let invert = polygon.invert

let points = polygon.exterior
let path = CGMutablePath()
path.move(to: points[0].cgPoint)
for point in points[1...] {
path.addLine(to: point.cgPoint)
}

// First we need to create a clip path, i.e., the area where we allowed to fill in the actual polygon.
// This is the whole frame *minus* the interior polygons.
// Useful example: https://samplecodebank.blogspot.com/2013/06/UIBezierPath-addClip-example.html
if !polygon.interiors.isEmpty {
let rect = CGMutablePath()
rect.move(to: frame.origin)
rect.addLine(to: CGPoint(x: frame.minX, y: frame.maxY))
rect.addLine(to: CGPoint(x: frame.maxX, y: frame.maxY))
rect.addLine(to: CGPoint(x: frame.maxX, y: frame.minY))

// First we need to create a clip path, i.e., the area where we allowed to fill in the actual polygon.
// This is the whole frame *minus* the interior polygons.
// Useful example: https://samplecodebank.blogspot.com/2013/06/UIBezierPath-addClip-example.html
if !polygon.interiors.isEmpty {
let rect = CGMutablePath()
rect.move(to: frame.origin)
rect.addLine(to: CGPoint(x: frame.minX, y: frame.maxY))
rect.addLine(to: CGPoint(x: frame.maxX, y: frame.maxY))
rect.addLine(to: CGPoint(x: frame.maxX, y: frame.minY))

for interior in polygon.interiors {
for interiorPoints in convertLine(interior.positions) {
rect.move(to: interiorPoints[0].cgPoint)
for point in interiorPoints[1...] {
rect.addLine(to: point.cgPoint)
}
}
for interiorPoints in polygon.interiors {
rect.move(to: interiorPoints[0].cgPoint)
for point in interiorPoints[1...] {
rect.addLine(to: point.cgPoint)
}

context.addPath(rect)
context.clip(using: .evenOdd)
}

// Then we can draw the polygon

context.addPath(path)
context.addPath(rect)
context.clip(using: .evenOdd)
}

// Then we can draw the polygon

context.addPath(path)

if let fillColor {
if invert {
// TODO: This doesn't actually invert. Would be nice to do that later.
context.setStrokeColor(fillColor)
context.setLineWidth(strokeWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.strokePath()

} else {
context.setFillColor(fillColor)
context.setLineWidth(0)
context.fillPath()
}

}

if let strokeColor {
context.addPath(path)
context.setStrokeColor(strokeColor)
if let fillColor {
if invert {
// TODO: This doesn't actually invert. Would be nice to do that later.
context.setStrokeColor(fillColor)
context.setLineWidth(strokeWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.strokePath()

} else {
context.setFillColor(fillColor)
context.setLineWidth(0)
context.fillPath()
}

}

if let strokeColor {
context.addPath(path)
context.setStrokeColor(strokeColor)
context.setLineWidth(strokeWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.strokePath()
}
}


func drawCircle(_ position: GeoJSON.Position, radius: CGFloat, fillColor: CGColor, strokeColor: CGColor? = nil, strokeWidth: Double = 2, in context: CGContext) {
guard let origin = converter(position) else { return }

context.addArc(center: origin.0.cgPoint, radius: radius / 2, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
func drawCircle(_ center: Point, radius: CGFloat, fillColor: CGColor, strokeColor: CGColor? = nil, strokeWidth: Double = 2, in context: CGContext) {
context.addArc(center: center.cgPoint, radius: radius / 2, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)

context.setFillColor(fillColor)
context.fillPath()

if let strokeColor {
context.addArc(center: origin.0.cgPoint, radius: radius / 2, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
context.addArc(center: center.cgPoint, radius: radius / 2, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
context.setStrokeColor(strokeColor)
context.setLineWidth(strokeWidth)
context.strokePath()
Expand Down Expand Up @@ -181,7 +189,11 @@ extension GeoDrawer {
extension GeoDrawer {

public func draw(_ contents: [Content], mapBackground: CGColor? = nil, mapOutline: CGColor? = nil, mapBackdrop: CGColor? = nil, in context: CGContext) {

let projected = contents.compactMap(project)
draw(projected, mapBackground: mapBackground, mapOutline: mapOutline, mapBackdrop: mapBackdrop, in: context)
}

func draw(_ contents: [ProjectedContent], mapBackground: CGColor?, mapOutline: CGColor?, mapBackdrop: CGColor?, in context: CGContext) {
let size = CGSize(width: self.size.width, height: self.size.height)
let bounds = CGRect(origin: .zero, size: size)

Expand All @@ -201,10 +213,14 @@ extension GeoDrawer {
switch content {
case .circle:
break // this will go above the outline, as they might go outside projection
case let .line(line, stroke, strokeWidth):
draw(line, strokeColor: stroke, strokeWidth: strokeWidth, in: context)
case let .polygon(polygon, fill, stroke, strokeWidth):
draw(polygon, fillColor: fill, strokeColor: stroke, strokeWidth: strokeWidth, frame: bounds, in: context)
case let .line(lines, stroke, strokeWidth):
for line in lines {
draw(line, strokeColor: stroke, strokeWidth: strokeWidth, in: context)
}
case let .polygon(polygons, fill, stroke, strokeWidth):
for polygon in polygons {
draw(polygon, fillColor: fill, strokeColor: stroke, strokeWidth: strokeWidth, frame: bounds, in: context)
}
}
}

Expand Down
63 changes: 60 additions & 3 deletions Sources/GeoDrawer/GeoDrawer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,72 @@ extension GeoDrawer {

}

// MARK: - Projected content

extension GeoDrawer {

struct ProjectedLineString: Hashable {
let points: [Point]
}

struct ProjectedPolygon: Hashable {
let exterior: [Point]
let interiors: [[Point]]

// In some projections such as Azimuthal, we might need to colour a cut-out
// rather than the projected polygon.
let invert: Bool
}

enum ProjectedContent: Hashable {
case line([ProjectedLineString], stroke: Color, strokeWidth: Double)
case polygon([ProjectedPolygon], fill: Color, stroke: Color?, strokeWidth: Double)
case circle(Point, radius: Double, fill: Color, stroke: Color?, strokeWidth: Double)
}
}

extension GeoDrawer {
func project(_ line: GeoJSON.LineString) -> [ProjectedLineString] {
let lines = convertLine(line.positions)
return lines.map(ProjectedLineString.init(points:))
}

func project(_ polygon: GeoJSON.Polygon) -> [ProjectedPolygon] {
let invert: Bool = invertCheck?(polygon) ?? false
let interiors = polygon.interiors.flatMap { convertLine($0.positions) }
return convertLine(polygon.exterior.positions).map { points in
return .init(exterior: points, interiors: interiors, invert: invert)
}
}

func project(_ content: Content) -> ProjectedContent? {
switch content {
case let .line(line, stroke, strokeWidth):
return .line(project(line), stroke: stroke, strokeWidth: strokeWidth)
case let .polygon(polygon, fill, stroke, strokeWidth):
return .polygon(project(polygon), fill: fill, stroke: stroke, strokeWidth: strokeWidth)
case let .circle(center, radius, fill, stroke, strokeWidth):
guard let point = converter(center)?.0 else { return nil }
return .circle(point, radius: radius, fill: fill, stroke: stroke, strokeWidth: strokeWidth)
}
}
}

// MARK: - Line helper

extension GeoDrawer {

private enum Grouping: Equatable {
enum Grouping: Equatable {
case wrapped
case notWrapped
case notProjected
}

/// - Returns: Typically returns a single element, but can return multiple, if the line wraps around
func convertLine(_ positions: [GeoJSON.Position]) -> [[Point]] {
Self.convertLine(positions, projection: projection, size: size, zoomTo: zoomTo, insets: insets, converter: converter)
}

private static func projectLine(_ positions: [GeoJSON.Position], projection: Projection) -> [(Point, Point?)] {

// 1. Turn degrees into radians
Expand All @@ -208,10 +264,11 @@ extension GeoDrawer {
}

/// - Returns: Typically returns a single element, but can return multiple, if the line wraps around
func convertLine(_ positions: [GeoJSON.Position]) -> [[Point]] {
private static func convertLine(_ positions: [GeoJSON.Position], projection: Projection?, size: Size, zoomTo: Rect?, insets: EdgeInsets, converter: (GeoJSON.Position) -> (Point, Bool)?) -> [[Point]] {

guard let projection else {
return [positions.compactMap {
self.converter($0)?.0
converter($0)?.0
}]
}

Expand Down
23 changes: 21 additions & 2 deletions Sources/GeoDrawer/GeoMap+UIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,32 @@ import GeoJSONKit

public class GeoMapView: UIView {
public var contents: [GeoDrawer.Content] = [] {
didSet { setNeedsDisplay(bounds) }
didSet {
_projected = nil
setNeedsDisplay(bounds)
}
}

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

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

public var insets: GeoProjector.EdgeInsets = .zero {
didSet {
_drawer = nil
_projected = nil
setNeedsDisplay(bounds)
}
}
Expand All @@ -56,10 +62,23 @@ public class GeoMapView: UIView {
public override var frame: CGRect {
didSet {
_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
}
}

private var _drawer: GeoDrawer!
private var drawer: GeoDrawer {
if let _drawer {
Expand Down Expand Up @@ -93,7 +112,7 @@ public class GeoMapView: UIView {

// Use Core Graphics functions to draw the content of your view
drawer.draw(
contents,
projected,
mapBackground: mapBackground.cgColor,
mapOutline: mapOutline.cgColor,
mapBackdrop: background.cgColor,
Expand Down

0 comments on commit 699e5d9

Please sign in to comment.