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
4 changes: 1 addition & 3 deletions .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ jobs:
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
args: --fix --quiet

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Example/ForeFlightKMLDemo/KML/KMLGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CoreLocation
import ForeFlightKML

enum KMLGenerator {
static func generateCircleKML(center: CLLocationCoordinate2D, radiusMeters: Double) -> String {
static func generateCircleKML(center: CLLocationCoordinate2D, radiusMeters: Double) throws -> Data? {
let builder = ForeFlightKMLBuilder(documentName: "Foreflight KML Demo")

let centerCoordinate = Coordinate(latitude: center.latitude, longitude: center.longitude)
Expand All @@ -23,7 +23,8 @@ enum KMLGenerator {
radiusMeters: radiusMeters * 2,
style: PolygonStyle(outlineColor: .black, fillColor: .warning.withAlpha(0.3)))

return builder.build()
return try builder.buildKMZ()

}

static func polygonCoordinatesForMap(center: CLLocationCoordinate2D, radiusMeters: Double) -> [CLLocationCoordinate2D] {
Expand Down
14 changes: 7 additions & 7 deletions Example/ForeFlightKMLDemo/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,19 @@ struct ContentView: View {

private func handleMapTap(_ coord: CLLocationCoordinate2D) {
lastTapCoordinate = coord
do {
let kmz = try KMLGenerator.generateCircleKML(center: coord, radiusMeters: defaultRadiusMeters)

let kml = KMLGenerator.generateCircleKML(center: coord, radiusMeters: defaultRadiusMeters)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).kmz")

let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).kml")
do {
try kml.data(using: .utf8)?.write(to: tmpURL)
try kmz?.write(to: tmpURL)
kmlToShareURL = tmpURL
showingShare = true
} catch {
print("Failed to write KML: \(error)")
print("Failed to write KMZ: \(error)")
kmlToShareURL = nil
}
}
Expand Down
9 changes: 9 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/florianreinhart/Geodesy", .upToNextMajor(from: "0.2.2"))
.package(url: "https://github.com/florianreinhart/Geodesy", .upToNextMajor(from: "0.2.2")),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0")
],
targets: [
.target(
name: "ForeFlightKML",
dependencies: [
.product(name: "Geodesy", package: "Geodesy")
.product(name: "Geodesy", package: "Geodesy"),
.product(name: "ZIPFoundation", package: "ZIPFoundation")
],
resources: [
.process("Resources")
]
),
.testTarget(
Expand Down
63 changes: 36 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,21 @@

> Swift framework to build KML files in Jeppesen ForeFlight friendly format.

This package provides a small, focused API for composing KML documents suitable for importing into ForeFlight as **User Map Shapes (KML)**. It intentionally avoids UI concerns — it gives you `String` KML output (or bytes) which your app can write to disk and share using the standard iOS share sheet.
This package provides a small, focused API for composing KML/KMZ documents suitable for importing into ForeFlight as **User Map Shapes (KML)**. It intentionally avoids UI concerns — it gives you `String` KML output (or bytes) which your app can write to disk and share using the standard iOS share sheet.

---

## Quick highlights

- Compose `Placemark`s with `Point`, `LineString`, `Polygon` and derived geometry helpers (circles, arc segments, etc.).
- Compose `Placemark`s with `Point`, `LineString`, `Polygon` and derived geometry helpers (circles, arc sectors, etc.).
- Create reusable styles (`Style`, `LineStyle`, `PolyStyle`, `IconStyle`, `LabelStyle`) and assign them to placemarks.
- `ForeFlightKMLBuilder` collects placemarks and styles, emits a complete `kml` document string.
- `ForeFlightKMLBuilder` collects placemarks and styles, emits a complete `kml` or `kmz` document .
- Lightweight — no UI code.

---

## Install

1. In Xcode: **File › Add Packages...**
2. Enter the repository URL.
3. Choose the `ForeFlightKML` package product and add it to your app target.

---

## Example Output
Using the example given on the [ForeFlight website](https://foreflight.com/support/user-map-shapes/) the below is generated using this Framework.
Expand All @@ -30,27 +25,23 @@ See `/Tests/ForeFlightKMLTests/UserMapShapesSampleFullTest.swift`

<img width="615" height="770" alt="Image" src="/docs/example-output.png" />

## Minimal Quick Start
## Quick Start

```swift
import ForeFlightKML
import Geodesy (we use this for Coordinate and relative positioniong)
import Geodesy (used for coordinates)

// Example: Airport Traffic Pattern
let builder = ForeFlightKMLBuilder(documentName: "Airport with ATZ")

// Runway centerline
builder.addLine(
name: "Runway 15-33",
coordinates: [Coordinate(latitude:, longitude:),Coordinate(latitude:, longitude:)],
style: LineStyle(color: .black)
)

// Traffic Warning Area
builder.addLineCircle(
name: "Airport ATZ",
center: airportCenter,
radiusMeters: 4630, // 2.5 nautical mile ATZ
center: Coordinate(latitude:, longitude:),
radiusMeters: 4630,
PolygonStyle(outlineColor: .black, fillColor: .warning.withAlpha(0.3))
)

Expand All @@ -60,19 +51,40 @@ presentShareSheet(with: url)
```

> **Note**: ForeFlight supports importing KML/KMZ files via the iOS share sheet. See ForeFlight's docs for exact import behavior.
---

## API quick reference (important types)

- `ForeFlightKMLBuilder` — builder for the KML document. Methods: `addPlacemark(_:)`, `kmlString()`.
## API Reference

### KMLBuidler
`ForeFlightKMLBuilder` is the builder for the KML/KMZ document.
- Document name can be set on `init` or with `setDocumentName()`
- Elements can be manually added using `addPlacemark(_:)`
- The output is accessed by: for KML `try builder.build()` or for KMZ: `try builder.buildKMZ()`

### KMLBuilder Convenience Elements
- `addPoint` Add a point with style.
- `addLine` Add a line connecting multiple coordinates.
- `addLineCircle` Add a circular line (approximated by line segments).
- `addLineSector` Add an arc sector line geometry.
- `addPolygon` Add a polygon with outer boundary and optional holes.
- `addPolygonCircle` Add a polygon with outer boundary and optional holes.
- `addPolygonSector` Add a filled sector polygon (pie slice).
- `addPolygonAnnularSector` Add a filled annular (ring) sector polygon.
- `addLabel` Add a text-only label placemark at a coordinate.

### ForeflightKMLBuilder Export formats
- `kml String` via `builder.build()`
- `kml Data` via `builder.kmlData()`
- `kmz Data` via `builder.buildKMZ()`
- KMZ (zipped KML) is required when using custom icons or using labelBadge (which uses a transparent .png under the hood).

### Underlying elements
- `Placemark` — a Feature containing a geometry (must implement `KMLElement`). Optionally attach a `KMLStyle`.
- Geometry types: `Point`, `Line`, `LineCircle`, `LineSegment` (segment of a Circle), `Polygon`, `PolygonCircle` (filled circle), `PolygonSegment` (filled segment) `LinearRing`.
- Geometry types: `Point`, `Line`, `LineCircle`, `LineSector` (sector of a Circle), `Polygon`, `PolygonCircle` (filled circle), `PolygonSector` (filled sector) `LinearRing`.
- `Style` and substyles: `LineStyle`, `PolyStyle`, `IconStyle`, `LabelStyle`.
- `KMLColor` — helper to create the aabbggrr color values used by KML.

Full public API surface is visible in the package sources; the README examples show common usage patterns.

---
Full public API surface is visible in the package sources.

## Notes, conventions and gotchas

Expand All @@ -81,13 +93,10 @@ Full public API surface is visible in the package sources; the README examples s
- **Angles/bearings**: bearings (for arc & circle generation) are interpreted in degrees (0..360). The bearing convention is clockwise from north.
- **Altitude**: When you provide altitudes, the `AltitudeMode` is emitted (defaults to `.absolute` in most geometries).
- **Styles**: `Style` generates a stable `id` when provided; otherwise a UUID-based id is generated. `ForeFlightKMLBuilder` will automatically register styles added via `Placemark`.
---

## Demo & tests

The repo contains an `Example` app that demonstrates building shapes and the `Tests` folder with unit tests. Before publishing, ensure the example builds and the tests run in CI.

---
The repo contains an `Example` app that demonstrates building shapes and the `Tests` folder with unit tests.

## Contributing

Expand Down
5 changes: 5 additions & 0 deletions Sources/ForeFlightKML/Errors/KMZExportError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public enum KMZExportError: Error {
case kmzRequired
case missingLocalResource(String)
case archiveCreationFailed
}
53 changes: 39 additions & 14 deletions Sources/ForeFlightKML/ForeFlightKML+Convenience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ extension ForeFlightKMLBuilder {
return addPlacemark(placemark)
}

/// Add an arc segment line geometry.
/// Add an arc sector line geometry.
/// - Parameters:
/// - name: Display name in ForeFlight (optional)
/// - center: Center point of the arc
Expand All @@ -110,7 +110,7 @@ extension ForeFlightKMLBuilder {
/// - style: Path style defining line appearance (optional)
/// - Returns: Self for method chaining
@discardableResult
public func addLineSegment(
public func addLineSector(
name: String? = nil,
center: Coordinate,
radiusMeters: Double,
Expand All @@ -124,7 +124,7 @@ extension ForeFlightKMLBuilder {
precondition(radiusMeters > 0, "Radius must be positive")
precondition(numberOfPoints >= 3, "Need at least 3 segments for an arc")

let segment = LineSegment(
let sector = LineSector(
center: center,
radius: radiusMeters,
startAngle: startAngle,
Expand All @@ -134,7 +134,7 @@ extension ForeFlightKMLBuilder {
tessellate: tessellate
)

let placemark = Placemark(name: name, geometry: segment, style: style)
let placemark = Placemark(name: name, geometry: sector, style: style)
return addPlacemark(placemark)
}

Expand Down Expand Up @@ -205,7 +205,7 @@ extension ForeFlightKMLBuilder {
return addPlacemark(placemark)
}

/// Add a filled segment polygon (pie slice).
/// Add a filled sector polygon (pie slice).
/// - Parameters:
/// - name: Display name in ForeFlight (optional)
/// - center: Center point of the arc
Expand All @@ -218,7 +218,7 @@ extension ForeFlightKMLBuilder {
/// - style: Polygon style defining outline and optional fill (optional)
/// - Returns: Self for method chaining
@discardableResult
public func addPolygonSegment(
public func addPolygonSector(
name: String? = nil,
center: Coordinate,
radiusMeters: Double,
Expand All @@ -232,7 +232,7 @@ extension ForeFlightKMLBuilder {
precondition(radiusMeters > 0, "Radius must be positive")
precondition(numberOfPoints >= 3, "Need at least 3 segments for a segment")

let segment = PolygonSegment(
let sector = PolygonSector(
center: center,
radius: radiusMeters,
startAngle: startAngle,
Expand All @@ -242,15 +242,15 @@ extension ForeFlightKMLBuilder {
tessellate: tessellate
)

let placemark = Placemark(name: name, geometry: segment, style: style)
let placemark = Placemark(name: name, geometry: sector, style: style)
return addPlacemark(placemark)
}

/// Add a filled annular (ring) segment polygon.
/// This creates a segment between two radii, excluding the inner circle area.
/// Add a filled annular (ring) sector polygon.
/// This creates a sector between two radii, excluding the inner circle area.
/// - Parameters:
/// - name: Display name in ForeFlight (optional)
/// - center: Center point of the segment
/// - center: Center point of the sector
/// - innerRadius: Inner radius in meters (the "hole" size)
/// - outerRadius: Outer radius in meters
/// - startAngle: Starting angle in degrees (0° = North, clockwise)
Expand All @@ -261,7 +261,7 @@ extension ForeFlightKMLBuilder {
/// - style: Polygon style defining outline and optional fill (optional)
/// - Returns: Self for method chaining
@discardableResult
public func addPolygonAnnularSegment(
public func addPolygonAnnularSector(
name: String? = nil,
center: Coordinate,
innerRadius: Double,
Expand All @@ -277,7 +277,7 @@ extension ForeFlightKMLBuilder {
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(
let sector = PolygonAnnularSector(
center: center,
innerRadius: innerRadius,
outerRadius: outerRadius,
Expand All @@ -288,7 +288,32 @@ extension ForeFlightKMLBuilder {
tessellate: tessellate
)

let placemark = Placemark(name: name, geometry: segment, style: style)
let placemark = Placemark(name: name, geometry: sector, style: style)
return addPlacemark(placemark)
}

/// Add a text-only label placemark at a coordinate.
/// This uses a transparent 1×1 icon to enable ForeFlight’s “badge” rendering, with the badge color driven by `IconStyle.color`.
/// - Important: Because this relies on a local icon asset, the output **must be exported as KMZ** (not plain KML) for the label to render correctly.
/// - Parameters:
/// - text: Label text displayed in ForeFlight
/// - coordinate: Geographic coordinate
/// - altitude: Altitude in meters (optional)
/// - color: Badge/background color for the label (default: white)
/// - Returns: Self for method chaining
@discardableResult
public func addLabel(
_ text: String,
coordinate: Coordinate,
altitude: Double? = nil,
color: KMLColor = .white
) -> Self {
addPoint(
name: text,
coordinate: coordinate,
altitude: altitude,
style: .labelBadge(color: color)
)
}

}
11 changes: 10 additions & 1 deletion Sources/ForeFlightKML/ForeFlightKML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,21 @@ public final class ForeFlightKMLBuilder {
return self
}

/// True if this document must be exported as KMZ to render correctly.
public var requiresKMZ: Bool {
styleManager.requiresKMZ
}

// MARK: - Build Methods

/// Generate the complete KML string for this document.
/// This method can be called multiple times - it doesn't modify the builder state.
/// - Returns: A complete KML document as a UTF-8 string ready for export to ForeFlight
public func build() -> String {
public func build() throws -> String {
guard !requiresKMZ else {
throw KMZExportError.kmzRequired
}

return kmlString()
}

Expand Down
Loading