From a34e348710588741c7211c46619125bef47e7fad Mon Sep 17 00:00:00 2001 From: Dan Leedham Date: Fri, 19 Dec 2025 20:22:55 +0000 Subject: [PATCH 1/6] Developers choose output format --- .../ForeFlightKMLDemo/KML/KMLGenerator.swift | 4 +-- .../ForeFlightKMLDemo/Views/ContentView.swift | 9 +++--- README.md | 11 +++---- Sources/ForeFlightKML/Enums/KMLFormat.swift | 4 +++ Sources/ForeFlightKML/Errors/BuildError.swift | 6 ++++ ...KMZExportError.swift => ExportError.swift} | 3 +- Sources/ForeFlightKML/ForeFlightKML.swift | 30 +++++++++++++++---- Sources/ForeFlightKML/ForeflightKML+KMZ.swift | 4 +-- Sources/ForeFlightKML/Utils/DataFormat.swift | 12 ++++++++ 9 files changed, 63 insertions(+), 20 deletions(-) create mode 100644 Sources/ForeFlightKML/Enums/KMLFormat.swift create mode 100644 Sources/ForeFlightKML/Errors/BuildError.swift rename Sources/ForeFlightKML/Errors/{KMZExportError.swift => ExportError.swift} (71%) create mode 100644 Sources/ForeFlightKML/Utils/DataFormat.swift diff --git a/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift b/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift index a12f5eb..77cb9e6 100644 --- a/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift +++ b/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift @@ -4,7 +4,7 @@ import CoreLocation import ForeFlightKML enum KMLGenerator { - static func generateCircleKML(center: CLLocationCoordinate2D, radiusMeters: Double) throws -> Data? { + static func generateCircleKML(center: CLLocationCoordinate2D, radiusMeters: Double) throws -> KMLBuildResult { let builder = ForeFlightKMLBuilder(documentName: "Foreflight KML Demo") let centerCoordinate = Coordinate(latitude: center.latitude, longitude: center.longitude) @@ -23,7 +23,7 @@ enum KMLGenerator { radiusMeters: radiusMeters * 2, style: PolygonStyle(outlineColor: .black, fillColor: .warning.withAlpha(0.3))) - return try builder.buildKMZ() + return try builder.build(as: .kmz) } diff --git a/Example/ForeFlightKMLDemo/Views/ContentView.swift b/Example/ForeFlightKMLDemo/Views/ContentView.swift index 80f2ab4..fda51f8 100644 --- a/Example/ForeFlightKMLDemo/Views/ContentView.swift +++ b/Example/ForeFlightKMLDemo/Views/ContentView.swift @@ -1,5 +1,6 @@ import SwiftUI import MapKit +import ForeFlightKML struct ContentView: View { @State private var lastTapCoordinate: CLLocationCoordinate2D? @@ -49,14 +50,14 @@ struct ContentView: View { private func handleMapTap(_ coord: CLLocationCoordinate2D) { lastTapCoordinate = coord do { - let kmz = try KMLGenerator.generateCircleKML(center: coord, radiusMeters: defaultRadiusMeters) + let buildResult = try 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())).kmz") - - try kmz?.write(to: tmpURL) + let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).\(buildResult.fileExtension)") + + try buildResult.data.write(to: tmpURL) kmlToShareURL = tmpURL showingShare = true } catch { diff --git a/README.md b/README.md index 01277ac..0ee0b53 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ builder.addLineCircle( ) let url = FileManager.default.temporaryDirectory.appendingPathComponent("shapes.kml") -try builder.build().write(to: url, atomically: true, encoding: .utf8) +try builder.build().write(to: url) presentShareSheet(with: url) ``` @@ -59,7 +59,7 @@ presentShareSheet(with: url) `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()` + - The output is accessed by `try builder.build()` ### KMLBuilder Convenience Elements - `addPoint` Add a point with style. @@ -73,9 +73,10 @@ presentShareSheet(with: url) - `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()` +- Type `KMLBuildResult` contains: `data: Data`, `fileExtension: String` and `mimetype: String` +- `kml Data` via `builder.build(as: .kml)` +- `kmz Data` via `builder.buildKMZ(as: .kmz)` +- `kml String` via `builder.kmlString()` - KMZ (zipped KML) is required when using custom icons or using labelBadge (which uses a transparent .png under the hood). ### Underlying elements diff --git a/Sources/ForeFlightKML/Enums/KMLFormat.swift b/Sources/ForeFlightKML/Enums/KMLFormat.swift new file mode 100644 index 0000000..9655def --- /dev/null +++ b/Sources/ForeFlightKML/Enums/KMLFormat.swift @@ -0,0 +1,4 @@ +public enum KMLFormat { + case kml + case kmz +} diff --git a/Sources/ForeFlightKML/Errors/BuildError.swift b/Sources/ForeFlightKML/Errors/BuildError.swift new file mode 100644 index 0000000..b1571f1 --- /dev/null +++ b/Sources/ForeFlightKML/Errors/BuildError.swift @@ -0,0 +1,6 @@ +enum BuildError: Error { + case missingAssetsForKMZ + case unsupportedFeatureForKML + case emptyArchive + case internalError(underlying: Error) +} diff --git a/Sources/ForeFlightKML/Errors/KMZExportError.swift b/Sources/ForeFlightKML/Errors/ExportError.swift similarity index 71% rename from Sources/ForeFlightKML/Errors/KMZExportError.swift rename to Sources/ForeFlightKML/Errors/ExportError.swift index ba3e86e..05af107 100644 --- a/Sources/ForeFlightKML/Errors/KMZExportError.swift +++ b/Sources/ForeFlightKML/Errors/ExportError.swift @@ -1,5 +1,6 @@ -public enum KMZExportError: Error { +public enum ExportError: Error { case kmzRequired case missingLocalResource(String) case archiveCreationFailed } + diff --git a/Sources/ForeFlightKML/ForeFlightKML.swift b/Sources/ForeFlightKML/ForeFlightKML.swift index 445d6b2..9b8c707 100644 --- a/Sources/ForeFlightKML/ForeFlightKML.swift +++ b/Sources/ForeFlightKML/ForeFlightKML.swift @@ -4,7 +4,7 @@ import GeodesySpherical /// A Builder for composing a KML Document (styles + placemarks). /// Use this to create KML documents compatible with ForeFlight's User Map Shapes feature. /// -public final class ForeFlightKMLBuilder { +public final class ForeFlightKMLBuilder: KMLBuilding { /// Optional name for the `` element. private var documentName: String? /// Collection of placemarks added to this builder. @@ -57,12 +57,30 @@ public final class ForeFlightKMLBuilder { /// 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() throws -> String { - guard !requiresKMZ else { - throw KMZExportError.kmzRequired + public func build(as format: KMLFormat = .kmz) throws -> KMLBuildResult { + if format == .kml, requiresKMZ { + throw BuildError.unsupportedFeatureForKML } - return kmlString() + switch format { + case .kml: + let data = buildKML() + return KMLBuildResult( + data: data, + fileExtension: "kml", + mimetype: "application/vnd.google-earth.kml+xml" + ) + + case .kmz: + guard let data = try buildKMZ() else { + throw BuildError.emptyArchive + } + return KMLBuildResult( + data: data, + fileExtension: "kmz", + mimetype: "application/vnd.google-earth.kmz" + ) + } } /// Produce the full KML string for this document. @@ -91,7 +109,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. /// - Returns: `Data` containing the encoded KML, or an empty `Data` if encoding fails. - public func kmlData(encoding: String.Encoding = .utf8) -> Data { + public func buildKML(encoding: String.Encoding = .utf8) -> Data { return kmlString().data(using: encoding) ?? Data() } diff --git a/Sources/ForeFlightKML/ForeflightKML+KMZ.swift b/Sources/ForeFlightKML/ForeflightKML+KMZ.swift index 300dac6..46cacdc 100644 --- a/Sources/ForeFlightKML/ForeflightKML+KMZ.swift +++ b/Sources/ForeFlightKML/ForeflightKML+KMZ.swift @@ -6,7 +6,7 @@ public extension ForeFlightKMLBuilder { /// Build a KMZ (ZIP) containing doc.kml and any required local assets. func buildKMZ() throws -> Data? { - let kmlData = kmlData() + let kmlData = buildKML() let archive = try Archive(accessMode: .create) @@ -38,7 +38,7 @@ private extension ForeFlightKMLBuilder { let bundle = Bundle.module guard let iconURL = bundle.url(forResource: "1x1", withExtension: "png") else { - throw KMZExportError.missingLocalResource("1x1.png") + throw ExportError.missingLocalResource("1x1.png") } try archive.addEntry( diff --git a/Sources/ForeFlightKML/Utils/DataFormat.swift b/Sources/ForeFlightKML/Utils/DataFormat.swift new file mode 100644 index 0000000..7f40d95 --- /dev/null +++ b/Sources/ForeFlightKML/Utils/DataFormat.swift @@ -0,0 +1,12 @@ +import Foundation + +public struct KMLBuildResult { + public let data: Data + public let fileExtension: String + public let mimetype: String +} + +protocol KMLBuilding { + func build(as format: KMLFormat) throws -> KMLBuildResult +} + From f5be19e471930bccd16ac4e55ee97b389a460eaf Mon Sep 17 00:00:00 2001 From: Dan Leedham Date: Fri, 19 Dec 2025 20:27:01 +0000 Subject: [PATCH 2/6] Move builder protocol --- Sources/ForeFlightKML/Utils/DataFormat.swift | 4 ---- Sources/ForeFlightKML/Utils/Protocols.swift | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ForeFlightKML/Utils/DataFormat.swift b/Sources/ForeFlightKML/Utils/DataFormat.swift index 7f40d95..d7947b7 100644 --- a/Sources/ForeFlightKML/Utils/DataFormat.swift +++ b/Sources/ForeFlightKML/Utils/DataFormat.swift @@ -6,7 +6,3 @@ public struct KMLBuildResult { public let mimetype: String } -protocol KMLBuilding { - func build(as format: KMLFormat) throws -> KMLBuildResult -} - diff --git a/Sources/ForeFlightKML/Utils/Protocols.swift b/Sources/ForeFlightKML/Utils/Protocols.swift index 3ea966d..0c52751 100644 --- a/Sources/ForeFlightKML/Utils/Protocols.swift +++ b/Sources/ForeFlightKML/Utils/Protocols.swift @@ -26,3 +26,7 @@ public protocol KMLSubStyle { // These produce the inner element tags (..., ...). func kmlString() -> String } + +protocol KMLBuilding { + func build(as format: KMLFormat) throws -> KMLBuildResult +} From e898307b716bb1cd6d38c624fbbf87aaaac3aecb Mon Sep 17 00:00:00 2001 From: Dan Leedham Date: Sat, 20 Dec 2025 13:11:07 +0000 Subject: [PATCH 3/6] Rename to Output Format --- .../Enums/{KMLFormat.swift => OutputFormat.swift} | 2 +- Sources/ForeFlightKML/ForeFlightKML.swift | 8 ++++---- Sources/ForeFlightKML/Utils/DataFormat.swift | 2 +- Sources/ForeFlightKML/Utils/Protocols.swift | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename Sources/ForeFlightKML/Enums/{KMLFormat.swift => OutputFormat.swift} (50%) diff --git a/Sources/ForeFlightKML/Enums/KMLFormat.swift b/Sources/ForeFlightKML/Enums/OutputFormat.swift similarity index 50% rename from Sources/ForeFlightKML/Enums/KMLFormat.swift rename to Sources/ForeFlightKML/Enums/OutputFormat.swift index 9655def..5ead170 100644 --- a/Sources/ForeFlightKML/Enums/KMLFormat.swift +++ b/Sources/ForeFlightKML/Enums/OutputFormat.swift @@ -1,4 +1,4 @@ -public enum KMLFormat { +public enum OutputFormat { case kml case kmz } diff --git a/Sources/ForeFlightKML/ForeFlightKML.swift b/Sources/ForeFlightKML/ForeFlightKML.swift index 9b8c707..4c2a830 100644 --- a/Sources/ForeFlightKML/ForeFlightKML.swift +++ b/Sources/ForeFlightKML/ForeFlightKML.swift @@ -4,7 +4,7 @@ import GeodesySpherical /// A Builder for composing a KML Document (styles + placemarks). /// Use this to create KML documents compatible with ForeFlight's User Map Shapes feature. /// -public final class ForeFlightKMLBuilder: KMLBuilding { +public final class ForeFlightKMLBuilder: Building { /// Optional name for the `` element. private var documentName: String? /// Collection of placemarks added to this builder. @@ -57,7 +57,7 @@ public final class ForeFlightKMLBuilder: KMLBuilding { /// 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(as format: KMLFormat = .kmz) throws -> KMLBuildResult { + public func build(as format: OutputFormat = .kmz) throws -> BuildResult { if format == .kml, requiresKMZ { throw BuildError.unsupportedFeatureForKML } @@ -65,7 +65,7 @@ public final class ForeFlightKMLBuilder: KMLBuilding { switch format { case .kml: let data = buildKML() - return KMLBuildResult( + return BuildResult( data: data, fileExtension: "kml", mimetype: "application/vnd.google-earth.kml+xml" @@ -75,7 +75,7 @@ public final class ForeFlightKMLBuilder: KMLBuilding { guard let data = try buildKMZ() else { throw BuildError.emptyArchive } - return KMLBuildResult( + return BuildResult( data: data, fileExtension: "kmz", mimetype: "application/vnd.google-earth.kmz" diff --git a/Sources/ForeFlightKML/Utils/DataFormat.swift b/Sources/ForeFlightKML/Utils/DataFormat.swift index d7947b7..103525c 100644 --- a/Sources/ForeFlightKML/Utils/DataFormat.swift +++ b/Sources/ForeFlightKML/Utils/DataFormat.swift @@ -1,6 +1,6 @@ import Foundation -public struct KMLBuildResult { +public struct BuildResult { public let data: Data public let fileExtension: String public let mimetype: String diff --git a/Sources/ForeFlightKML/Utils/Protocols.swift b/Sources/ForeFlightKML/Utils/Protocols.swift index 0c52751..cfa6ff8 100644 --- a/Sources/ForeFlightKML/Utils/Protocols.swift +++ b/Sources/ForeFlightKML/Utils/Protocols.swift @@ -27,6 +27,6 @@ public protocol KMLSubStyle { func kmlString() -> String } -protocol KMLBuilding { - func build(as format: KMLFormat) throws -> KMLBuildResult +protocol Building { + func build(as format: OutputFormat) throws -> BuildResult } From a5d323ba09d921e7ce59c1e6911864aa19ed61de Mon Sep 17 00:00:00 2001 From: Dan Leedham Date: Sat, 20 Dec 2025 13:18:52 +0000 Subject: [PATCH 4/6] Add new tests --- Package.swift | 4 + Sources/ForeFlightKML/Errors/BuildError.swift | 15 ++- .../Builder/ForeFlightKML+BuildTests.swift | 92 +++++++++++++++++++ .../ForeFlightKML+KMZTests.swift | 0 .../{ => Builder}/ForeFlightKMLTests.swift | 0 .../CoreElementTests/PolygonTests.swift | 20 ++++ .../{ => Utils}/TestHelpers.swift | 0 .../UserMapShapesSampleFullTest.swift | 2 +- .../UserMapShapesSampleIndividualTests.swift | 0 9 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 Tests/ForeFlightKMLTests/Builder/ForeFlightKML+BuildTests.swift rename Tests/ForeFlightKMLTests/{ => Builder}/ForeFlightKML+KMZTests.swift (100%) rename Tests/ForeFlightKMLTests/{ => Builder}/ForeFlightKMLTests.swift (100%) rename Tests/ForeFlightKMLTests/{ => Utils}/TestHelpers.swift (100%) rename Tests/{ForeFlightKMLTests => UserMapShapeTests}/UserMapShapesSampleFullTest.swift (99%) rename Tests/{ForeFlightKMLTests => UserMapShapeTests}/UserMapShapesSampleIndividualTests.swift (100%) diff --git a/Package.swift b/Package.swift index 71dbf62..03bc596 100644 --- a/Package.swift +++ b/Package.swift @@ -28,6 +28,10 @@ let package = Package( .testTarget( name: "ForeFlightKMLTests", dependencies: ["ForeFlightKML"] + ), + .testTarget( + name: "UserMapShapeTests", + dependencies: ["ForeFlightKML"] ) ] ) diff --git a/Sources/ForeFlightKML/Errors/BuildError.swift b/Sources/ForeFlightKML/Errors/BuildError.swift index b1571f1..967a09b 100644 --- a/Sources/ForeFlightKML/Errors/BuildError.swift +++ b/Sources/ForeFlightKML/Errors/BuildError.swift @@ -1,6 +1,19 @@ -enum BuildError: Error { +enum BuildError: Error, Equatable { case missingAssetsForKMZ case unsupportedFeatureForKML case emptyArchive case internalError(underlying: Error) + + static func == (lhs: BuildError, rhs: BuildError) -> Bool { + switch (lhs, rhs) { + case (.missingAssetsForKMZ, .missingAssetsForKMZ): return true + case (.unsupportedFeatureForKML, .unsupportedFeatureForKML): return true + case (.emptyArchive, .emptyArchive): return true + case (.internalError, .internalError): + // Consider any internalError equal regardless of underlying Error + return true + default: + return false + } + } } diff --git a/Tests/ForeFlightKMLTests/Builder/ForeFlightKML+BuildTests.swift b/Tests/ForeFlightKMLTests/Builder/ForeFlightKML+BuildTests.swift new file mode 100644 index 0000000..f95b2e6 --- /dev/null +++ b/Tests/ForeFlightKMLTests/Builder/ForeFlightKML+BuildTests.swift @@ -0,0 +1,92 @@ +import XCTest +import GeodesySpherical +@testable import ForeFlightKML + +final class ForeFlightKMLBuildTests: XCTestCase { + + func test_build_asKMZ_returnsExpectedMetadataAndHasDocKML() throws { + let builder = ForeFlightKMLBuilder() + builder.addPoint( + name: "Normal", + coordinate: Coordinate(latitude: 51.0, longitude: -1.0), + altitude: 0, + style: PointStyle(icon: .custom(type: .square, color: .white, scale: 1.0)) + ) + + let result = try builder.build(as: .kmz) + XCTAssertEqual(result.fileExtension, "kmz") + XCTAssertEqual(result.mimetype, "application/vnd.google-earth.kmz") + // Quick sanity check that archive contains doc.kml + let archive = try ArchiveDataVerifier.archive(from: result.data) + XCTAssertNotNil(archive["doc.kml"], "KMZ must contain doc.kml at root") + } + + func test_build_asKML_whenNoKMZRequired_succeedsWithMetadata() throws { + let builder = ForeFlightKMLBuilder() + // Use a simple predefined icon that should not require local assets + builder.addPoint( + name: "Simple", + coordinate: Coordinate(latitude: 0, longitude: 0) + ) + + XCTAssertFalse(builder.requiresKMZ) + let result = try builder.build(as: .kml) + XCTAssertEqual(result.fileExtension, "kml") + XCTAssertEqual(result.mimetype, "application/vnd.google-earth.kml+xml") + // Resulting data should decode as UTF-8 and contain KML root + let kml = String(data: result.data, encoding: .utf8) + XCTAssertNotNil(kml) + XCTAssertTrue(kml?.contains("My Doc")) + } + + func test_clear_resetsState_counts_and_requiresKMZ() { + let builder = ForeFlightKMLBuilder() + builder.addPoint(name: "p", coordinate: Coordinate(latitude: 0, longitude: 0)) + builder.addLabel("Badge", coordinate: Coordinate(latitude: 1, longitude: 1), color: .warning) + + XCTAssertGreaterThan(builder.placemarkCount, 0) + XCTAssertGreaterThan(builder.styleCount, 0) + XCTAssertTrue(builder.requiresKMZ) + + builder.clear() + + XCTAssertEqual(builder.placemarkCount, 0) + XCTAssertEqual(builder.styleCount, 0) + XCTAssertFalse(builder.requiresKMZ) + } +} + +// MARK: - Local helper +import ZIPFoundation +private enum ArchiveDataVerifier { + static func archive(from data: Data) throws -> Archive { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("kmz") + try data.write(to: url) + return try Archive(url: url, accessMode: .read) + } +} diff --git a/Tests/ForeFlightKMLTests/ForeFlightKML+KMZTests.swift b/Tests/ForeFlightKMLTests/Builder/ForeFlightKML+KMZTests.swift similarity index 100% rename from Tests/ForeFlightKMLTests/ForeFlightKML+KMZTests.swift rename to Tests/ForeFlightKMLTests/Builder/ForeFlightKML+KMZTests.swift diff --git a/Tests/ForeFlightKMLTests/ForeFlightKMLTests.swift b/Tests/ForeFlightKMLTests/Builder/ForeFlightKMLTests.swift similarity index 100% rename from Tests/ForeFlightKMLTests/ForeFlightKMLTests.swift rename to Tests/ForeFlightKMLTests/Builder/ForeFlightKMLTests.swift diff --git a/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift b/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift index a9b0e83..ca98f99 100644 --- a/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift +++ b/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift @@ -97,4 +97,24 @@ final class PolygonTests: XCTestCase { XCTAssertFalse(kml.contains("")) } + + func test_polygon_withInnerRings_producesInnerBoundary() { + let builder = ForeFlightKMLBuilder() + let outer = [ + Coordinate(latitude: 0, longitude: 0), + Coordinate(latitude: 0, longitude: 1), + Coordinate(latitude: 1, longitude: 1) + ] + let hole = [ + Coordinate(latitude: 0.2, longitude: 0.2), + Coordinate(latitude: 0.2, longitude: 0.8), + Coordinate(latitude: 0.8, longitude: 0.8) + ] + + builder.addPolygon(name: "poly", outerRing: outer, innerRings: [hole]) + let kml = builder.kmlString() + XCTAssertTrue(kml.contains("")) + XCTAssertTrue(kml.contains("")) + } } + diff --git a/Tests/ForeFlightKMLTests/TestHelpers.swift b/Tests/ForeFlightKMLTests/Utils/TestHelpers.swift similarity index 100% rename from Tests/ForeFlightKMLTests/TestHelpers.swift rename to Tests/ForeFlightKMLTests/Utils/TestHelpers.swift diff --git a/Tests/ForeFlightKMLTests/UserMapShapesSampleFullTest.swift b/Tests/UserMapShapeTests/UserMapShapesSampleFullTest.swift similarity index 99% rename from Tests/ForeFlightKMLTests/UserMapShapesSampleFullTest.swift rename to Tests/UserMapShapeTests/UserMapShapesSampleFullTest.swift index e350347..7d9f4ed 100644 --- a/Tests/ForeFlightKMLTests/UserMapShapesSampleFullTest.swift +++ b/Tests/UserMapShapeTests/UserMapShapesSampleFullTest.swift @@ -341,7 +341,7 @@ final class UserMapShapesRecreationTests: XCTestCase { ) ) - let kml = try builder.build() + let kml = builder.kmlString() let tempDir = FileManager.default.temporaryDirectory let fileURL = tempDir.appendingPathComponent("FFIconTest_Generated.kml") diff --git a/Tests/ForeFlightKMLTests/UserMapShapesSampleIndividualTests.swift b/Tests/UserMapShapeTests/UserMapShapesSampleIndividualTests.swift similarity index 100% rename from Tests/ForeFlightKMLTests/UserMapShapesSampleIndividualTests.swift rename to Tests/UserMapShapeTests/UserMapShapesSampleIndividualTests.swift From 305fb8dc4b8066d8143982573c9eca53f2545b7c Mon Sep 17 00:00:00 2001 From: Dan Leedham Date: Sat, 20 Dec 2025 13:19:21 +0000 Subject: [PATCH 5/6] Lint --- Sources/ForeFlightKML/Errors/ExportError.swift | 1 - Sources/ForeFlightKML/Utils/DataFormat.swift | 1 - Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ForeFlightKML/Errors/ExportError.swift b/Sources/ForeFlightKML/Errors/ExportError.swift index 05af107..2fd7800 100644 --- a/Sources/ForeFlightKML/Errors/ExportError.swift +++ b/Sources/ForeFlightKML/Errors/ExportError.swift @@ -3,4 +3,3 @@ public enum ExportError: Error { case missingLocalResource(String) case archiveCreationFailed } - diff --git a/Sources/ForeFlightKML/Utils/DataFormat.swift b/Sources/ForeFlightKML/Utils/DataFormat.swift index 103525c..bf80777 100644 --- a/Sources/ForeFlightKML/Utils/DataFormat.swift +++ b/Sources/ForeFlightKML/Utils/DataFormat.swift @@ -5,4 +5,3 @@ public struct BuildResult { public let fileExtension: String public let mimetype: String } - diff --git a/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift b/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift index ca98f99..1f7ceea 100644 --- a/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift +++ b/Tests/ForeFlightKMLTests/CoreElementTests/PolygonTests.swift @@ -97,7 +97,7 @@ final class PolygonTests: XCTestCase { XCTAssertFalse(kml.contains("")) } - + func test_polygon_withInnerRings_producesInnerBoundary() { let builder = ForeFlightKMLBuilder() let outer = [ @@ -117,4 +117,3 @@ final class PolygonTests: XCTestCase { XCTAssertTrue(kml.contains("")) } } - From b725cb00b4811be01efb40f9596e41df185c9be2 Mon Sep 17 00:00:00 2001 From: Dan Leedham Date: Sat, 20 Dec 2025 13:27:00 +0000 Subject: [PATCH 6/6] Update Readme --- .../ForeFlightKMLDemo/KML/KMLGenerator.swift | 2 +- README.md | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift b/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift index 77cb9e6..e328461 100644 --- a/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift +++ b/Example/ForeFlightKMLDemo/KML/KMLGenerator.swift @@ -4,7 +4,7 @@ import CoreLocation import ForeFlightKML enum KMLGenerator { - static func generateCircleKML(center: CLLocationCoordinate2D, radiusMeters: Double) throws -> KMLBuildResult { + static func generateCircleKML(center: CLLocationCoordinate2D, radiusMeters: Double) throws -> BuildResult { let builder = ForeFlightKMLBuilder(documentName: "Foreflight KML Demo") let centerCoordinate = Coordinate(latitude: center.latitude, longitude: center.longitude) diff --git a/README.md b/README.md index 0ee0b53..eafd548 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This package provides a small, focused API for composing KML/KMZ documents suita ## Example Output Using the example given on the [ForeFlight website](https://foreflight.com/support/user-map-shapes/) the below is generated using this Framework. -See `/Tests/ForeFlightKMLTests/UserMapShapesSampleFullTest.swift` +See `/Tests/UserMapShapeTests/UserMapShapesSampleFullTest.swift` Image @@ -32,6 +32,7 @@ import ForeFlightKML import Geodesy (used for coordinates) let builder = ForeFlightKMLBuilder(documentName: "Airport with ATZ") + builder.addLine( name: "Runway 15-33", coordinates: [Coordinate(latitude:, longitude:),Coordinate(latitude:, longitude:)], @@ -45,8 +46,10 @@ builder.addLineCircle( PolygonStyle(outlineColor: .black, fillColor: .warning.withAlpha(0.3)) ) -let url = FileManager.default.temporaryDirectory.appendingPathComponent("shapes.kml") -try builder.build().write(to: url) +let buildResult = try builder.build(as: .kmz) + +let url = FileManager.default.temporaryDirectory.appendingPathComponent("shapes\(buildResult.fileExtension)") +try buildResult.data.write(to: tmpURL) presentShareSheet(with: url) ``` @@ -73,10 +76,16 @@ presentShareSheet(with: url) - `addLabel` Add a text-only label placemark at a coordinate. ### ForeflightKMLBuilder Export formats -- Type `KMLBuildResult` contains: `data: Data`, `fileExtension: String` and `mimetype: String` +Type `BuildResult` contains: +``` + data: Data + fileExtension: String + mimetype: String +``` +Specific data access: - `kml Data` via `builder.build(as: .kml)` -- `kmz Data` via `builder.buildKMZ(as: .kmz)` -- `kml String` via `builder.kmlString()` +- `kmz Data` via `builder.build(as: .kmz)` +- `kml String` via `builder.kmlString()` note: this can be unsafe and should only be used for debugging - KMZ (zipped KML) is required when using custom icons or using labelBadge (which uses a transparent .png under the hood). ### Underlying elements @@ -97,7 +106,7 @@ Full public API surface is visible in the package sources. ## Demo & tests -The repo contains an `Example` app that demonstrates building shapes and the `Tests` folder with unit tests. +The repo contains an `Example` app that demonstrates building shapes and the `Tests` folder with unit and end to end example tests. ## Contributing