Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance by projecting contents just once #31

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading