Skip to content

Commit 8a8fb77

Browse files
authored
Version 0.4.0 (#9)
* Update example app to use remote dependency * Embed image in repo (#2) * Add function for annular sectors (#4) * Add Swiftlint * Enable adding label without icon (#7) * Enable export by KMZ (#7) * Update readme * Use Sector and Segment correctly (#8)
1 parent 25a2e22 commit 8a8fb77

36 files changed

Lines changed: 449 additions & 117 deletions

.github/workflows/swiftlint.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- uses: actions/checkout@v1
16-
- name: GitHub Action for SwiftLint
17-
uses: norio-nomura/action-swiftlint@3.2.1
1816
- name: GitHub Action for SwiftLint with --strict
1917
uses: norio-nomura/action-swiftlint@3.2.1
2018
with:
21-
args: --strict
19+
args: --fix --quiet

Example/ForeFlightKMLDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Example/ForeFlightKMLDemo/KML/KMLGenerator.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import CoreLocation
44
import ForeFlightKML
55

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

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

26-
return builder.build()
26+
return try builder.buildKMZ()
27+
2728
}
2829

2930
static func polygonCoordinatesForMap(center: CLLocationCoordinate2D, radiusMeters: Double) -> [CLLocationCoordinate2D] {

Example/ForeFlightKMLDemo/Views/ContentView.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,19 @@ struct ContentView: View {
4848

4949
private func handleMapTap(_ coord: CLLocationCoordinate2D) {
5050
lastTapCoordinate = coord
51+
do {
52+
let kmz = try KMLGenerator.generateCircleKML(center: coord, radiusMeters: defaultRadiusMeters)
5153

52-
let kml = KMLGenerator.generateCircleKML(center: coord, radiusMeters: defaultRadiusMeters)
54+
let dateFormatter = DateFormatter()
55+
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
5356

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

57-
let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).kml")
58-
do {
59-
try kml.data(using: .utf8)?.write(to: tmpURL)
59+
try kmz?.write(to: tmpURL)
6060
kmlToShareURL = tmpURL
6161
showingShare = true
6262
} catch {
63-
print("Failed to write KML: \(error)")
63+
print("Failed to write KMZ: \(error)")
6464
kmlToShareURL = nil
6565
}
6666
}

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ let package = Package(
1111
)
1212
],
1313
dependencies: [
14-
.package(url: "https://github.com/florianreinhart/Geodesy", .upToNextMajor(from: "0.2.2"))
14+
.package(url: "https://github.com/florianreinhart/Geodesy", .upToNextMajor(from: "0.2.2")),
15+
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0")
1516
],
1617
targets: [
1718
.target(
1819
name: "ForeFlightKML",
1920
dependencies: [
20-
.product(name: "Geodesy", package: "Geodesy")
21+
.product(name: "Geodesy", package: "Geodesy"),
22+
.product(name: "ZIPFoundation", package: "ZIPFoundation")
23+
],
24+
resources: [
25+
.process("Resources")
2126
]
2227
),
2328
.testTarget(

README.md

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,21 @@
22

33
> Swift framework to build KML files in Jeppesen ForeFlight friendly format.
44
5-
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.
5+
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.
66

7-
---
87

98
## Quick highlights
109

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

16-
---
17-
1815
## Install
19-
2016
1. In Xcode: **File › Add Packages...**
2117
2. Enter the repository URL.
2218
3. Choose the `ForeFlightKML` package product and add it to your app target.
2319

24-
---
2520

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

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

33-
## Minimal Quick Start
28+
## Quick Start
3429

3530
```swift
3631
import ForeFlightKML
37-
import Geodesy (we use this for Coordinate and relative positioniong)
32+
import Geodesy (used for coordinates)
3833

39-
// Example: Airport Traffic Pattern
4034
let builder = ForeFlightKMLBuilder(documentName: "Airport with ATZ")
41-
42-
// Runway centerline
4335
builder.addLine(
4436
name: "Runway 15-33",
4537
coordinates: [Coordinate(latitude:, longitude:),Coordinate(latitude:, longitude:)],
4638
style: LineStyle(color: .black)
4739
)
4840

49-
// Traffic Warning Area
5041
builder.addLineCircle(
5142
name: "Airport ATZ",
52-
center: airportCenter,
53-
radiusMeters: 4630, // 2.5 nautical mile ATZ
43+
center: Coordinate(latitude:, longitude:),
44+
radiusMeters: 4630,
5445
PolygonStyle(outlineColor: .black, fillColor: .warning.withAlpha(0.3))
5546
)
5647

@@ -60,19 +51,40 @@ presentShareSheet(with: url)
6051
```
6152

6253
> **Note**: ForeFlight supports importing KML/KMZ files via the iOS share sheet. See ForeFlight's docs for exact import behavior.
63-
---
6454
65-
## API quick reference (important types)
6655

67-
- `ForeFlightKMLBuilder` — builder for the KML document. Methods: `addPlacemark(_:)`, `kmlString()`.
56+
## API Reference
57+
58+
### KMLBuidler
59+
`ForeFlightKMLBuilder` is the builder for the KML/KMZ document.
60+
- Document name can be set on `init` or with `setDocumentName()`
61+
- Elements can be manually added using `addPlacemark(_:)`
62+
- The output is accessed by: for KML `try builder.build()` or for KMZ: `try builder.buildKMZ()`
63+
64+
### KMLBuilder Convenience Elements
65+
- `addPoint` Add a point with style.
66+
- `addLine` Add a line connecting multiple coordinates.
67+
- `addLineCircle` Add a circular line (approximated by line segments).
68+
- `addLineSector` Add an arc sector line geometry.
69+
- `addPolygon` Add a polygon with outer boundary and optional holes.
70+
- `addPolygonCircle` Add a polygon with outer boundary and optional holes.
71+
- `addPolygonSector` Add a filled sector polygon (pie slice).
72+
- `addPolygonAnnularSector` Add a filled annular (ring) sector polygon.
73+
- `addLabel` Add a text-only label placemark at a coordinate.
74+
75+
### ForeflightKMLBuilder Export formats
76+
- `kml String` via `builder.build()`
77+
- `kml Data` via `builder.kmlData()`
78+
- `kmz Data` via `builder.buildKMZ()`
79+
- KMZ (zipped KML) is required when using custom icons or using labelBadge (which uses a transparent .png under the hood).
80+
81+
### Underlying elements
6882
- `Placemark` — a Feature containing a geometry (must implement `KMLElement`). Optionally attach a `KMLStyle`.
69-
- Geometry types: `Point`, `Line`, `LineCircle`, `LineSegment` (segment of a Circle), `Polygon`, `PolygonCircle` (filled circle), `PolygonSegment` (filled segment) `LinearRing`.
83+
- Geometry types: `Point`, `Line`, `LineCircle`, `LineSector` (sector of a Circle), `Polygon`, `PolygonCircle` (filled circle), `PolygonSector` (filled sector) `LinearRing`.
7084
- `Style` and substyles: `LineStyle`, `PolyStyle`, `IconStyle`, `LabelStyle`.
7185
- `KMLColor` — helper to create the aabbggrr color values used by KML.
7286

73-
Full public API surface is visible in the package sources; the README examples show common usage patterns.
74-
75-
---
87+
Full public API surface is visible in the package sources.
7688

7789
## Notes, conventions and gotchas
7890

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

8697
## Demo & tests
8798

88-
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.
89-
90-
---
99+
The repo contains an `Example` app that demonstrates building shapes and the `Tests` folder with unit tests.
91100

92101
## Contributing
93102

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public enum KMZExportError: Error {
2+
case kmzRequired
3+
case missingLocalResource(String)
4+
case archiveCreationFailed
5+
}

Sources/ForeFlightKML/ForeFlightKML+Convenience.swift

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ extension ForeFlightKMLBuilder {
9797
return addPlacemark(placemark)
9898
}
9999

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

127-
let segment = LineSegment(
127+
let sector = LineSector(
128128
center: center,
129129
radius: radiusMeters,
130130
startAngle: startAngle,
@@ -134,7 +134,7 @@ extension ForeFlightKMLBuilder {
134134
tessellate: tessellate
135135
)
136136

137-
let placemark = Placemark(name: name, geometry: segment, style: style)
137+
let placemark = Placemark(name: name, geometry: sector, style: style)
138138
return addPlacemark(placemark)
139139
}
140140

@@ -205,7 +205,7 @@ extension ForeFlightKMLBuilder {
205205
return addPlacemark(placemark)
206206
}
207207

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

235-
let segment = PolygonSegment(
235+
let sector = PolygonSector(
236236
center: center,
237237
radius: radiusMeters,
238238
startAngle: startAngle,
@@ -242,15 +242,15 @@ extension ForeFlightKMLBuilder {
242242
tessellate: tessellate
243243
)
244244

245-
let placemark = Placemark(name: name, geometry: segment, style: style)
245+
let placemark = Placemark(name: name, geometry: sector, style: style)
246246
return addPlacemark(placemark)
247247
}
248248

249-
/// Add a filled annular (ring) segment polygon.
250-
/// This creates a segment between two radii, excluding the inner circle area.
249+
/// Add a filled annular (ring) sector polygon.
250+
/// This creates a sector between two radii, excluding the inner circle area.
251251
/// - Parameters:
252252
/// - name: Display name in ForeFlight (optional)
253-
/// - center: Center point of the segment
253+
/// - center: Center point of the sector
254254
/// - innerRadius: Inner radius in meters (the "hole" size)
255255
/// - outerRadius: Outer radius in meters
256256
/// - startAngle: Starting angle in degrees (0° = North, clockwise)
@@ -261,7 +261,7 @@ extension ForeFlightKMLBuilder {
261261
/// - style: Polygon style defining outline and optional fill (optional)
262262
/// - Returns: Self for method chaining
263263
@discardableResult
264-
public func addPolygonAnnularSegment(
264+
public func addPolygonAnnularSector(
265265
name: String? = nil,
266266
center: Coordinate,
267267
innerRadius: Double,
@@ -277,7 +277,7 @@ extension ForeFlightKMLBuilder {
277277
precondition(outerRadius > innerRadius, "Outer radius must be greater than inner radius")
278278
precondition(numberOfPoints >= 3, "Need at least 3 segments for an annular segment")
279279

280-
let segment = PolygonAnnularSegment(
280+
let sector = PolygonAnnularSector(
281281
center: center,
282282
innerRadius: innerRadius,
283283
outerRadius: outerRadius,
@@ -288,7 +288,32 @@ extension ForeFlightKMLBuilder {
288288
tessellate: tessellate
289289
)
290290

291-
let placemark = Placemark(name: name, geometry: segment, style: style)
291+
let placemark = Placemark(name: name, geometry: sector, style: style)
292292
return addPlacemark(placemark)
293293
}
294+
295+
/// Add a text-only label placemark at a coordinate.
296+
/// This uses a transparent 1×1 icon to enable ForeFlight’s “badge” rendering, with the badge color driven by `IconStyle.color`.
297+
/// - 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.
298+
/// - Parameters:
299+
/// - text: Label text displayed in ForeFlight
300+
/// - coordinate: Geographic coordinate
301+
/// - altitude: Altitude in meters (optional)
302+
/// - color: Badge/background color for the label (default: white)
303+
/// - Returns: Self for method chaining
304+
@discardableResult
305+
public func addLabel(
306+
_ text: String,
307+
coordinate: Coordinate,
308+
altitude: Double? = nil,
309+
color: KMLColor = .white
310+
) -> Self {
311+
addPoint(
312+
name: text,
313+
coordinate: coordinate,
314+
altitude: altitude,
315+
style: .labelBadge(color: color)
316+
)
317+
}
318+
294319
}

Sources/ForeFlightKML/ForeFlightKML.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,21 @@ public final class ForeFlightKMLBuilder {
4747
return self
4848
}
4949

50+
/// True if this document must be exported as KMZ to render correctly.
51+
public var requiresKMZ: Bool {
52+
styleManager.requiresKMZ
53+
}
54+
5055
// MARK: - Build Methods
5156

5257
/// Generate the complete KML string for this document.
5358
/// This method can be called multiple times - it doesn't modify the builder state.
5459
/// - Returns: A complete KML document as a UTF-8 string ready for export to ForeFlight
55-
public func build() -> String {
60+
public func build() throws -> String {
61+
guard !requiresKMZ else {
62+
throw KMZExportError.kmzRequired
63+
}
64+
5665
return kmlString()
5766
}
5867

0 commit comments

Comments
 (0)