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
13 changes: 11 additions & 2 deletions .github/workflows/restrict-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@ on:
pull_request:
branches:
- main
# edited: re-check when an existing PR is retargeted to main.
types: [opened, synchronize, reopened, edited]

permissions: {}

jobs:
check-branch:
runs-on: ubuntu-latest
steps:
- name: Ensure PR comes from develop
# Passed through env, not ${{ }} in the script: branch names are set by
# whoever opens the PR, so interpolating one into the shell would run it.
env:
HEAD_REF: ${{ github.head_ref }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
if [ "${{ github.head_ref }}" != "develop" ]; then
echo "Only develop can be merged into main."
if [ "$HEAD_REF" != "develop" ] || [ "$HEAD_REPO" != "$GITHUB_REPOSITORY" ]; then
echo "Only develop can be merged into main (got '$HEAD_REF' from $HEAD_REPO)."
exit 1
fi
12 changes: 6 additions & 6 deletions .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ on:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
workflow_dispatch:
workflow_dispatch:

jobs:
SwiftLint:
runs-on: ubuntu-latest
# Official SwiftLint image, pinned to match local development.
container: ghcr.io/realm/swiftlint:0.65.1
steps:
- uses: actions/checkout@v1
- name: GitHub Action for SwiftLint with --strict
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --fix --quiet
- uses: actions/checkout@v7
- name: SwiftLint --strict
run: swiftlint lint --strict --reporter github-actions-logging
45 changes: 45 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Tests

on:
pull_request:
branches:
- develop
- main
push:
branches:
- develop
- main
workflow_dispatch:

concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: swift test (macOS) + iOS build
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@v7

# Package.swift needs Swift 6.2 tools, which ship with Xcode 26.
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.x'

- name: Toolchain
run: |
xcodebuild -version
swift --version

# Tests use XMLDocument, which is macOS-only, so they run on macOS rather than iOS.
- name: Test (macOS)
run: swift test --parallel

- name: Build (iOS Simulator)
run: |
xcodebuild build \
-scheme ForeFlightKML \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO
4 changes: 2 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
excluded:
- .build
- Tests/ForeFlightKMLTests/UserMapShapesSampleIndividualTests.swift
- Tests/ForeFlightKMLTests/UserMapShapesSampleFullTest.swift
- Tests/UserMapShapeTests/UserMapShapesSampleIndividualTests.swift
- Tests/UserMapShapeTests/UserMapShapesSampleFullTest.swift
- Example/ForeFlightKMLDemo/
identifier_name:
min_length: 1
Expand Down

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

1 change: 0 additions & 1 deletion Example/ForeFlightKMLDemo/KML/KMLGenerator.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import GeodesySpherical
import CoreLocation
import ForeFlightKML

Expand Down
1 change: 0 additions & 1 deletion Example/ForeFlightKMLDemo/Map/MapViewRepresentable.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import SwiftUI
import MapKit
import CoreLocation
import GeodesySpherical
import ForeFlightKML

struct MapViewRepresentable: UIViewRepresentable {
Expand Down
11 changes: 6 additions & 5 deletions Package.resolved

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

8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
// swift-tools-version:5.7
// swift-tools-version:6.2
import PackageDescription

let package = Package(
name: "ForeFlightKML",
platforms: [.iOS(.v15)],
platforms: [.iOS(.v15), .macOS(.v12)],
products: [
.library(
name: "ForeFlightKML",
targets: ["ForeFlightKML"]
)
],
dependencies: [
.package(url: "https://github.com/florianreinhart/Geodesy", .upToNextMajor(from: "0.2.2")),
.package(url: "https://github.com/Haelix-Code/Vincenty", .upToNextMinor(from: "0.0.1")),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0")
],
targets: [
.target(
name: "ForeFlightKML",
dependencies: [
.product(name: "Geodesy", package: "Geodesy"),
.product(name: "Vincenty", package: "Vincenty"),
.product(name: "ZIPFoundation", package: "ZIPFoundation")
],
resources: [
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This package provides a small, focused API for composing KML/KMZ documents suita
- 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` or `kmz` document.
- Thread-safe — `ForeFlightKMLBuilder` is `Sendable`, so KML generation can run off the main thread.
- Swift 6 concurrency-ready — geometry, styles, `Placemark` and `BuildResult` are `Sendable`, so KML generation can run off the main thread.
- Lightweight — no UI code.

## Install
Expand All @@ -30,7 +30,6 @@ See `/Tests/UserMapShapeTests/UserMapShapesSampleFullTest.swift`

```swift
import ForeFlightKML
import GeodesySpherical

let builder = ForeFlightKMLBuilder(documentName: "Airport with ATZ")

Expand Down Expand Up @@ -120,13 +119,13 @@ Full public API surface is visible in the package sources.
## Performance

- **Buffer-based generation** — KML is built using a single mutable `String` buffer rather than array concatenation, avoiding intermediate allocations.
- **Sendable** — `ForeFlightKMLBuilder` conforms to `Sendable`, allowing KML/KMZ generation to run on a background thread for a responsive UI.
- **Background generation** — build KML/KMZ off the main thread for a responsive UI. Geometry, styles, `Placemark` and `BuildResult` are `Sendable`. `ForeFlightKMLBuilder` deliberately isn't (it holds unsynchronised mutable state): create and build it within one task, or hand it to another task or actor and stop using it.
- **Smart compression** — small documents (< 100 KB) skip DEFLATE compression in KMZ output, reducing overhead.
- **Efficient geometry** — circle point generation uses direct arithmetic rather than `Measurement` conversions.

## Notes, conventions and gotchas

- **Coordinates order**: KML requires `longitude,latitude[,altitude]`. The public API accepts `Coordinate(latitude:..., longitude:...)` (from `Geodesy`) and the framework emits coordinates in the KML `lon,lat[,alt]` order.
- **Coordinates order**: KML requires `longitude,latitude[,altitude]`. The public API accepts `Coordinate(latitude:..., longitude:...)` (a typealias for [Vincenty](https://github.com/Haelix-Code/Vincenty)'s `GeodeticCoordinate`) and the framework emits coordinates in the KML `lon,lat[,alt]` order.
- **Units**: Distances (e.g. `LineCircle.radius`) are in **meters**.
- **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).
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/CoreElements/LineString.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// A line string connecting multiple coordinates in order.
/// Used for routes, boundaries, or any connected line geometry.
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/CoreElements/LinearRing.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// 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.
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/CoreElements/Point.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// A geographic point in 3D space that can be displayed as a marker in ForeFlight.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Enums/OutputFormat.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public enum OutputFormat {
public enum OutputFormat: Sendable {
case kml
case kmz
}
8 changes: 5 additions & 3 deletions Sources/ForeFlightKML/ForeFlightKML+Convenience.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import GeodesySpherical
import Vincenty

extension ForeFlightKMLBuilder {

Expand Down Expand Up @@ -293,8 +293,10 @@ extension ForeFlightKMLBuilder {
}

/// 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.
/// 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
Expand Down
11 changes: 6 additions & 5 deletions Sources/ForeFlightKML/ForeFlightKML.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import Foundation
import GeodesySpherical
import Vincenty

/// A Builder for composing a KML Document (styles + placemarks).
/// Use this to create KML documents compatible with ForeFlight's User Map Shapes feature.
///
/// - Note: Marked `@unchecked Sendable` because instances are intended
/// to be created, populated, and built within a single scope (not shared across threads).
/// This allows callers to use the builder from any actor/thread context.
public final class ForeFlightKMLBuilder: Building, @unchecked Sendable {
/// - Note: Not `Sendable`: the builder holds unsynchronised mutable state, so create, populate
/// and build it within a single scope. It can still be handed off to another task or actor
/// as long as the sender stops using it. The `Placemark`s and styles it takes, and the
/// `BuildResult` it produces, are all `Sendable`.
public final class ForeFlightKMLBuilder: Building {
/// Optional name for the `<Document>` element.
private var documentName: String?
/// Maximum decimal places for coordinate values. Trailing zeros are trimmed,
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Geometry/Circle/LineCircle.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import GeodesySpherical
import Vincenty

/// A circular line geometry approximated with multiple line segments.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Geometry/Circle/PolygonCircle.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// A circular polygon geometry approximated with multiple line segments.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Geometry/Segment/LineSector.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// An arc or sector geometry (like a pie slice) defined by center, radius, and angular extents.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

public struct PolygonAnnularSector: KMLElement, AltitudeSupport {
let polygon: Polygon
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Geometry/Segment/PolygonSector.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

public struct PolygonSector: KMLElement, AltitudeSupport {
let polygon: Polygon
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Geometry/Shared/CircleGeometry.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

internal enum CircleGeometry {

Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Geometry/Shared/Line.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// A simple line connecting multiple coordinates in sequence.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Geometry/Shared/SectorGeometry.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import GeodesySpherical
import Vincenty

internal enum SectorGeometry {
static func generateSectorPoints(
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Models/AltitudeSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension AltitudeSupport {

/// Supported Altitude Modes per KML 2.2 specification
/// See: https://developers.google.com/kml/documentation/kmlreference#altitudemode
public enum AltitudeMode: String, Equatable {
public enum AltitudeMode: String, Equatable, Sendable {
case absolute
case clampToGround
case relativeToGround
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Models/CoordinateContainer.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// Protocol for KML elements that contain coordinate arrays and support altitude/tessellation.
/// Used by LineString, LinearRing, and similar coordinate-based geometries.
Expand Down
4 changes: 2 additions & 2 deletions Sources/ForeFlightKML/Models/Placemark.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GeodesySpherical
import Vincenty

/// Represents a Feature Placemark that combines geometry with styling and metadata.
///
Expand All @@ -7,7 +7,7 @@ import GeodesySpherical
/// - Optional name that appears in ForeFlight
/// - Optional style that controls visual appearance
///
public struct Placemark {
public struct Placemark: Sendable {
public private(set) var name: String?
public private(set) var geometry: KMLElement
public private(set) var styleUrl: String?
Expand Down
2 changes: 1 addition & 1 deletion Sources/ForeFlightKML/Styles/Colors.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public struct KMLColor: Equatable, CustomStringConvertible {
public struct KMLColor: Equatable, CustomStringConvertible, Sendable {
public var description: String { kmlHexString }

/// Single internal storage: bits [31..24]=A, [23..16]=R, [15..8]=G, [7..0]=B
Expand Down
6 changes: 3 additions & 3 deletions Sources/ForeFlightKML/Styles/IconStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public struct IconStyle: KMLSubStyle {

/// Icon shapes that work with Google's predefined color system.
/// These are the "paddle" style icons that only support fixed colors.
public enum PredefinedIconType: String, Codable {
public enum PredefinedIconType: String, Codable, Sendable {
case pushpin
case circle
case square
Expand All @@ -102,7 +102,7 @@ public enum PredefinedIconType: String, Codable {

/// Icon shapes that support custom colors.
/// These are the "shapes" style icons that can be any color via KML's color tag.
public enum CustomIconType: Codable {
public enum CustomIconType: Codable, Sendable {
case opendiamond
case triangle
case forbidden
Expand Down Expand Up @@ -155,7 +155,7 @@ public enum CustomIconType: Codable {

/// Predefined icon colors available for paddle-style icons.
/// These are the standard colors provided by Google's KML icon set.
public enum DefinedIconColor: String, Codable {
public enum DefinedIconColor: String, Codable, Sendable {
case purple = "purple"
case white = "wht"
case green = "grn"
Expand Down
Loading
Loading