Skip to content
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
21 changes: 21 additions & 0 deletions .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: SwiftLint

on:
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
workflow_dispatch:

jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: GitHub Action for SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
- name: GitHub Action for SwiftLint with --strict
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
8 changes: 8 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
excluded:
- .build
- Tests/ForeFlightKMLTests/UserMapShapesSampleIndividualTests.swift
- Tests/ForeFlightKMLTests/UserMapShapesSampleFullTest.swift
- Example/ForeFlightKMLDemo/
identifier_name:
min_length: 1
max_length: 40
20 changes: 15 additions & 5 deletions Example/ForeFlightKMLDemo/KML/KMLGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ import ForeFlightKML
enum KMLGenerator {
static func generateCircleKML(center: CLLocationCoordinate2D, radiusMeters: Double) -> String {
let builder = ForeFlightKMLBuilder(documentName: "Foreflight KML Demo")

let centerCoordinate = Coordinate(latitude: center.latitude, longitude: center.longitude)

builder.addLineCircle(name: "Circle", center: centerCoordinate, radiusMeters: radiusMeters, numberOfPoints: 100, style: PathStyle(color: .black))

builder.addPolygonCircle(name: "Filled Circle" ,center: centerCoordinate, radiusMeters: radiusMeters * 2, style: PolygonStyle(outlineColor: .black, fillColor: .warning.withAlpha(0.3)))

builder.addLineCircle(
name: "Circle",
center: centerCoordinate,
radiusMeters: radiusMeters,
numberOfPoints: 100,
style: PathStyle(color: .black)
)

builder.addPolygonCircle(
name: "Filled Circle",
center: centerCoordinate,
radiusMeters: radiusMeters * 2,
style: PolygonStyle(outlineColor: .black, fillColor: .warning.withAlpha(0.3)))

return builder.build()
}

Expand Down
4 changes: 3 additions & 1 deletion Example/ForeFlightKMLDemo/Map/MapViewRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ struct MapViewRepresentable: UIViewRepresentable {
map.delegate = context.coordinator
map.showsUserLocation = true
map.pointOfInterestFilter = .excludingAll
map.setRegion(MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.750188, longitude: -1.581566), latitudinalMeters: 2000, longitudinalMeters: 2000),animated: false)
map.setRegion(MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 51.750188, longitude: -1.581566), latitudinalMeters: 2000, longitudinalMeters: 2000),
animated: false)

let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
tap.numberOfTapsRequired = 1
Expand Down
4 changes: 2 additions & 2 deletions Example/ForeFlightKMLDemo/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ struct ContentView: View {
lastTapCoordinate = coord

let kml = KMLGenerator.generateCircleKML(center: coord, radiusMeters: defaultRadiusMeters)

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"

let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).kml")
do {
try kml.data(using: .utf8)?.write(to: tmpURL)
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ let package = Package(
.testTarget(
name: "ForeFlightKMLTests",
dependencies: ["ForeFlightKML"]
),
)
]
)
9 changes: 5 additions & 4 deletions Sources/ForeFlightKML/CoreElements/LinearRing.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import GeodesySpherical

/// Defines a closed line string, typically the outer boundary of a Polygon. Optionally, a LinearRing can also be used as the inner boundary of a Polygon to create holes in the Polygon. A Polygon can contain multiple <LinearRing> elements used as inner boundaries.
/// Defines a closed line string, typically the outer boundary of a Polygon.
/// Optionally, a LinearRing can also be used as the inner boundary of a Polygon to create holes in the Polygon.
/// A Polygon can contain multiple <LinearRing> elements used as inner boundaries.
/// Note: LinearRing does NOT support altitudeMode - that is specified at the Polygon level.
/// However, individual coordinates can still have altitude values.
public struct LinearRing: CoordinateContainer {
Expand All @@ -9,7 +11,7 @@ public struct LinearRing: CoordinateContainer {
public var altitude: Double?
/// LinearRing doesn't define altitude mode (handled by parent Polygon)
public var altitudeMode: AltitudeMode? { nil }
public var tessellate: Bool? = nil
public var tessellate: Bool?

/// Create a new linear ring.
/// - Parameters:
Expand All @@ -21,8 +23,7 @@ public struct LinearRing: CoordinateContainer {

// Auto-close the ring if needed
if let first = coordinates.first, let last = coordinates.last,
first.latitude != last.latitude || first.longitude != last.longitude
{
first.latitude != last.latitude || first.longitude != last.longitude {
self.coordinates = coordinates + [first]
} else {
self.coordinates = coordinates
Expand Down
46 changes: 46 additions & 0 deletions Sources/ForeFlightKML/ForeFlightKML+Convenience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,50 @@ extension ForeFlightKMLBuilder {
let placemark = Placemark(name: name, geometry: segment, style: style)
return addPlacemark(placemark)
}

/// Add a filled annular (ring) segment polygon.
/// This creates a segment between two radii, excluding the inner circle area.
/// - Parameters:
/// - name: Display name in ForeFlight (optional)
/// - center: Center point of the segment
/// - innerRadius: Inner radius in meters (the "hole" size)
/// - outerRadius: Outer radius in meters
/// - startAngle: Starting angle in degrees (0° = North, clockwise)
/// - endAngle: Ending angle in degrees (0° = North, clockwise)
/// - numberOfPoints: Number of points for each arc (default: 64)
/// - altitude: Altitude in meters (optional)
/// - tessellate: Whether to follow ground contours (default: false)
/// - style: Polygon style defining outline and optional fill (optional)
/// - Returns: Self for method chaining
@discardableResult
public func addPolygonAnnularSegment(
name: String? = nil,
center: Coordinate,
innerRadius: Double,
outerRadius: Double,
startAngle: Double,
endAngle: Double,
numberOfPoints: Int = 64,
altitude: Double? = nil,
tessellate: Bool = false,
style: PolygonStyle? = nil
) -> Self {
precondition(innerRadius > 0, "Inner radius must be positive")
precondition(outerRadius > innerRadius, "Outer radius must be greater than inner radius")
precondition(numberOfPoints >= 3, "Need at least 3 segments for an annular segment")

let segment = PolygonAnnularSegment(
center: center,
innerRadius: innerRadius,
outerRadius: outerRadius,
startAngle: startAngle,
endAngle: endAngle,
numberOfPoints: numberOfPoints,
altitude: altitude,
tessellate: tessellate
)

let placemark = Placemark(name: name, geometry: segment, style: style)
return addPlacemark(placemark)
}
}
4 changes: 2 additions & 2 deletions Sources/ForeFlightKML/ForeFlightKML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public final class ForeFlightKMLBuilder {
"xmlns": "http://www.opengis.net/kml/2.2",
"xmlns:gx": "http://www.google.com/kml/ext/2.2",
"xmlns:kml": "http://www.opengis.net/kml/2.2",
"xmlns:atom": "http://www.w3.org/2005/Atom",
"xmlns:atom": "http://www.w3.org/2005/Atom"
]

/// Create a new builder.
Expand Down Expand Up @@ -80,7 +80,7 @@ public final class ForeFlightKMLBuilder {
}

/// Produce the KML document as `Data` using the given text encoding.
/// - Parameter encoding: The `String.Encoding` to use when converting the KML string into data. Defaults to `.utf8`.
/// - Parameter encoding: The `String.Encoding` to use when converting the KML string into data.
/// - Returns: `Data` containing the encoded KML, or an empty `Data` if encoding fails.
public func kmlData(encoding: String.Encoding = .utf8) -> Data {
return kmlString().data(using: encoding) ?? Data()
Expand Down
34 changes: 34 additions & 0 deletions Sources/ForeFlightKML/Geometry/PolygonAnnularSegment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import GeodesySpherical

public struct PolygonAnnularSegment: KMLElement, AltitudeSupport {
let polygon: Polygon

public var altitudeMode: AltitudeMode? { polygon.altitudeMode }
public init(
center: Coordinate,
innerRadius: Double,
outerRadius: Double,
startAngle: Double,
endAngle: Double,
numberOfPoints: Int = 100,
altitude: Double? = nil,
altitudeMode: AltitudeMode? = nil,
tessellate: Bool? = nil
) {
let coordinates = SegmentGeometry.generateAnnularSegmentPoints(
center: center,
innerRadius: innerRadius,
outerRadius: outerRadius,
startAngle: startAngle,
endAngle: endAngle,
numberOfPoints: numberOfPoints
)

let ring = LinearRing(coordinates: coordinates, altitude: altitude)
self.polygon = Polygon(outer: ring, altitudeMode: altitudeMode, tessellate: tessellate)
}

public func kmlString() -> String {
return polygon.kmlString()
}
}
3 changes: 1 addition & 2 deletions Sources/ForeFlightKML/Geometry/Shared/CircleGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ internal enum CircleGeometry {
}

if let first = circlePoints.first, let last = circlePoints.last,
first.longitude != last.longitude || first.latitude != last.latitude
{
first.longitude != last.longitude || first.latitude != last.latitude {
circlePoints.append(first)
}

Expand Down
45 changes: 45 additions & 0 deletions Sources/ForeFlightKML/Geometry/Shared/SegmentGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,49 @@ internal enum SegmentGeometry {
segmentPoints.append(center)
return segmentPoints
}

// swiftlint:disable:next function_parameter_count
static func generateAnnularSegmentPoints(
center: Coordinate,
innerRadius: Double,
outerRadius: Double,
startAngle: Double,
endAngle: Double,
numberOfPoints: Int
) -> [Coordinate] {
precondition(innerRadius > 0, "Inner radius must be positive")
precondition(outerRadius > innerRadius, "Outer radius must be greater than inner radius")

var points: [Coordinate] = []

let start = startAngle.truncatingRemainder(dividingBy: 360)
let end = endAngle.truncatingRemainder(dividingBy: 360)

let angleSpan: Double
if end >= start {
angleSpan = end - start
} else {
angleSpan = (360 - start) + end
}

for i in 0...numberOfPoints {
let fraction = Double(i) / Double(numberOfPoints)
let currentAngle = start + fraction * angleSpan
let point = center.destination(with: outerRadius, bearing: currentAngle)
points.append(point)
}

for i in 0...numberOfPoints {
let fraction = Double(i) / Double(numberOfPoints)
let currentAngle = end - fraction * angleSpan
let point = center.destination(with: innerRadius, bearing: currentAngle)
points.append(point)
}

if let first = points.first {
points.append(first)
}

return points
}
}
7 changes: 4 additions & 3 deletions Sources/ForeFlightKML/Styles/Colors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ public struct KMLColor: Equatable, CustomStringConvertible {
/// - alpha: Alpha component (0.0-1.0, will be clamped). Default is 1.0 (opaque)
/// - Returns: A new KMLColor
public static func fromRGB(red: Double, green: Double, blue: Double, alpha: Double = 1.0)
-> KMLColor
{
-> KMLColor {
func clampAndScale(_ value: Double) -> Int {
Int((max(0.0, min(1.0, value)) * 255.0).rounded())
}
Expand All @@ -69,6 +68,7 @@ public struct KMLColor: Equatable, CustomStringConvertible {
)
}

// swiftlint:disable function_body_length
/// Create a color from a hex string
/// - Parameter hex: Hex string in format "#RGB", "#RRGGBB", "#AARRGGBB", "RGB", "RRGGBB", or "AARRGGBB"
/// - Returns: Result containing KMLColor
Expand Down Expand Up @@ -147,6 +147,7 @@ public struct KMLColor: Equatable, CustomStringConvertible {
return .black
}
}
// swiftlint:enable function_body_length

/// Create a color from KML hex format (aabbggrr)
/// - Parameter kmlHex: Hex string in KML format "aabbggrr"
Expand Down Expand Up @@ -220,7 +221,7 @@ public struct KMLColor: Equatable, CustomStringConvertible {
switch self {
case .invalidHexFormat(let hex):
return
"Invalid hex color format: '\(hex)'. Expected formats: RGB, RRGGBB, AARRGGBB (with optional # prefix)"
"Invalid hex color format: '\(hex)'. Expected formats: RGB, RRGGBB, AARRGGBB (optional # prefix)"
case .invalidKMLHexFormat(let hex):
return
"Invalid KML hex color format: '\(hex)'. Expected format: aabbggrr (8 hex characters)"
Expand Down
9 changes: 4 additions & 5 deletions Sources/ForeFlightKML/Styles/IconStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ public struct IconStyle: KMLSubStyle {
/// - scale: Size multiplier for the icon (default: 1.3)
/// - Returns: A new IconStyle configured for custom colored shapes
public static func custom(type: CustomIconType, color: KMLColor? = nil, scale: Double? = 1.3)
-> IconStyle
{
-> IconStyle {
let baseUrl = "http://maps.google.com/mapfiles/kml/"
let href = "\(baseUrl)shapes/\(type.href).png"
return IconStyle(href: href, color: color, scale: scale)
Expand Down Expand Up @@ -87,9 +86,9 @@ public struct IconStyle: KMLSubStyle {
/// These are the "paddle" style icons that only support fixed colors.
public enum PredefinedIconType: String {
case pushpin
case circle = "circle"
case square = "square"
case diamond = "diamond"
case circle
case square
case diamond
}

/// Icon shapes that support custom colors.
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Styles/LabelStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct LabelStyle: KMLSubStyle {
let lines = [
"<LabelStyle>",
"<color>\(color.kmlHexString)</color>",
"</LabelStyle>",
"</LabelStyle>"
]
return lines.joined(separator: "\n")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Styles/LineStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public struct LineStyle: KMLSubStyle {
"<LineStyle>",
"<color>\(color.kmlHexString)</color>",
width.map { "<width>\($0)</width>" },
"</LineStyle>",
"</LineStyle>"
]
return lines.compactMap { $0 }.joined(separator: "\n")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Styles/PolyStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public struct PolyStyle: KMLSubStyle {
"<PolyStyle>",
"<color>\(color.kmlHexString)</color>",
fill.map { "<fill>\($0 ? 1 : 0)</fill>" },
"</PolyStyle>",
"</PolyStyle>"
]
return lines.compactMap { $0 }.joined(separator: "\n")
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/ForeFlightKML/Utils/Geodesy+KML.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import GeodesySpherical

// KML uses 3D geographic coordinates: longitude, latitude and altitude, in that order. The longitude and latitude components are in decimal degrees as defined by the World Geodetic System of 1984 (WGS-84). The vertical component (altitude) is measured in meters from the WGS84 EGM96 Geoid vertical datum.
// KML uses 3D geographic coordinates: longitude, latitude and altitude, in that order.
// The longitude and latitude components are in decimal degrees
// as defined by the World Geodetic System of 1984 (WGS-84).
// The vertical component (altitude) is measured in meters from the WGS84 EGM96 Geoid vertical datum.

extension GeodesySpherical.Coordinate {
public func kmlString() -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ final class LineStringTests: XCTestCase {
func testKmlWithAltitudeAndTessellate() {
let coords = [
Coordinate(latitude: 2, longitude: 3),
Coordinate(latitude: 4, longitude: 5),
Coordinate(latitude: 4, longitude: 5)
]
let line = LineString(
coordinates: coords, altitude: 100.0, altitudeMode: .absolute, tessellate: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class LinearRingTests: XCTestCase {
[-98.96485321080908, 30.49650491542987],
[-98.95965359046227, 30.92214152160733],
[-99.09548335615463, 31.45369338953584],
[-100.1097399038377, 31.57870338920791],
[-100.1097399038377, 31.57870338920791]
]

let coords = points.map { pair in
Expand Down
Loading