diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme
index 2629d5f..4a68db5 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme
@@ -202,6 +202,34 @@
ReferencedContainer = "container:">
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
diff --git a/Package.resolved b/Package.resolved
index 4ccdcfd..b41318f 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,5 +1,32 @@
{
"pins" : [
+ {
+ "identity" : "pathkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/kylef/PathKit.git",
+ "state" : {
+ "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
+ "version" : "1.0.1"
+ }
+ },
+ {
+ "identity" : "spectre",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/kylef/Spectre.git",
+ "state" : {
+ "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7",
+ "version" : "0.10.1"
+ }
+ },
+ {
+ "identity" : "stencil",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/stencilproject/Stencil",
+ "state" : {
+ "revision" : "4f222ac85d673f35df29962fc4c36ccfdaf9da5b",
+ "version" : "0.15.1"
+ }
+ },
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
diff --git a/Package.swift b/Package.swift
index 359ba0e..55621ac 100644
--- a/Package.swift
+++ b/Package.swift
@@ -17,6 +17,7 @@ let package = Package(
.library(name: "WGS84", targets: ["WGS84"]),
.library(name: "GeodeticGeometry", targets: ["GeodeticGeometry"]),
.library(name: "Turf", targets: ["Turf"]),
+ .executable(name: "generate-epsg", targets: ["GenerateEPSG"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-algorithms", .upToNextMajor(from: "1.0.0")),
@@ -29,6 +30,7 @@ let package = Package(
url: "https://github.com/pointfreeco/swift-snapshot-testing",
.upToNextMajor(from: "1.9.0")
),
+ .package(url: "https://github.com/stencilproject/Stencil", .upToNextMajor(from: "0.15.1")),
],
targets: [
// Targets are the basic building blocks of a package.
@@ -137,5 +139,10 @@ let package = Package(
"GeodeticConversions",
"WGS84Conversions",
]),
+
+ // ⚙️ Generate type definitions for EPSG Coordinate Reference Systems
+ .executableTarget(name: "GenerateEPSG", dependencies: [
+ .product(name: "Stencil", package: "Stencil"),
+ ]),
]
)
diff --git a/Sources/EPSG/.gitkeep b/Sources/EPSG/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Sources/GenerateEPSG/DTO.swift b/Sources/GenerateEPSG/DTO.swift
new file mode 100644
index 0000000..1c00b02
--- /dev/null
+++ b/Sources/GenerateEPSG/DTO.swift
@@ -0,0 +1,282 @@
+//
+// DTO.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 06/12/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import struct Foundation.URL
+
+// MARK: - /v1/CoordRefSystem
+
+struct SearchResults {
+ let results: [SearchResult]
+ let count: Int
+ let page: Int
+ let pageSize: Int
+ let totalResults: Int
+ let links: [Link]
+}
+extension SearchResults: Decodable {
+ enum CodingKeys: String, CodingKey {
+ case results = "Results"
+ case count = "Count"
+ case page = "Page"
+ case pageSize = "PageSize"
+ case totalResults = "TotalResults"
+ case links = "Links"
+ }
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.results = try container.decodeIfPresent([SearchResult].self, forKey: .results) ?? []
+ self.count = try container.decode(Int.self, forKey: .count)
+ self.page = try container.decode(Int.self, forKey: .page)
+ self.pageSize = try container.decode(Int.self, forKey: .pageSize)
+ self.totalResults = try container.decode(Int.self, forKey: .totalResults)
+ self.links = try container.decodeIfPresent([Link].self, forKey: .links) ?? []
+ }
+}
+
+struct CRSType: RawRepresentable, Decodable, CustomStringConvertible, CustomDebugStringConvertible {
+ static let swiftTypes: [String: String] = [
+ "geographic 2D": "TwoDimensionalCRS",
+ ]
+ let rawValue: String
+ var isSupported: Bool { Self.swiftTypes.keys.contains(self.rawValue) }
+ var swiftType: String { Self.swiftTypes[self.rawValue, default: "CoordinateReferenceSystem"] }
+ var description: String { self.swiftType }
+ var debugDescription: String { String(reflecting: self.rawValue) }
+}
+
+struct SearchResult {
+ let code: Int
+ let name: String?
+ let type: CRSType
+ let dataDource: String?
+ let area: String?
+ let remarks: String?
+ let deprecated: Bool
+ let superseded: Bool
+ let revisionDate: String?
+ let links: [Link]
+}
+extension SearchResult: Decodable {
+ enum CodingKeys: String, CodingKey {
+ case code = "Code"
+ case name = "Name"
+ case type = "Type"
+ case dataDource = "DataSource"
+ case area = "Area"
+ case remarks = "Remarks"
+ case deprecated = "Deprecated"
+ case superseded = "Superseded"
+ case revisionDate = "RevisionDate"
+ case links = "Links"
+ }
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.code = try container.decode(Int.self, forKey: .code)
+ self.name = try container.decodeIfPresent(String.self, forKey: .name)
+ self.type = try container.decode(CRSType.self, forKey: .type)
+ self.dataDource = try container.decodeIfPresent(String.self, forKey: .dataDource)
+ self.area = try container.decodeIfPresent(String.self, forKey: .area)
+ self.remarks = try container.decodeIfPresent(String.self, forKey: .remarks)
+ self.deprecated = try container.decodeIfPresent(Bool.self, forKey: .deprecated) ?? false
+ self.superseded = try container.decodeIfPresent(Bool.self, forKey: .superseded) ?? false
+ self.revisionDate = try container.decodeIfPresent(String.self, forKey: .revisionDate)
+ self.links = try container.decodeIfPresent([Link].self, forKey: .links) ?? []
+ }
+}
+
+struct Link: Decodable {
+ enum CodingKeys: String, CodingKey {
+ case rel, href
+ }
+
+ let rel: String?
+ let href: URL
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.rel = try container.decodeIfPresent(String.self, forKey: .rel)
+ self.href = try container.decode(URL.self, forKey: .href)
+ }
+}
+
+// MARK: - /v1/GeodeticCoordRefSystem
+
+struct GeographicCoordRefSystem: Decodable {
+ enum CodingKeys: String, CodingKey {
+ case datum = "Datum"
+ case datumEnsemble = "DatumEnsemble"
+ case baseCoordRefSystem = "BaseCoordRefSystem"
+ case conversion = "Conversion"
+ case geoidModels = "GeoidModels"
+ case usage = "Usage"
+ case coordSys = "CoordSys"
+ case kind = "Kind"
+ case deformations = "Deformations"
+ case code = "Code"
+ case changes = "Changes"
+ case alias = "Alias"
+ case links = "Links"
+ case name = "Name"
+ case remark = "Remark"
+ case dataSource = "DataSource"
+ case informationSource = "InformationSource"
+ case revisionDate = "RevisionDate"
+ case deprecations = "Deprecations"
+ case supersessions = "Supersessions"
+ }
+
+ let datum: ChildLink?
+ let datumEnsemble: ChildLink?
+ let baseCoordRefSystem: ChildLink?
+ let conversion: ChildLink?
+ let geoidModels: [ChildLink]
+ let usage: [Usage]
+ let coordSys: ChildLink?
+ let kind: String?
+ let deformations: [ChildLink]
+ let code: Int
+ let changes: [ChildLinkBase]
+ let alias: [AliasDetails]
+ let links: [Link]
+ let name: String?
+ let remark: String?
+ let dataSource: String?
+ let informationSource: String?
+ let revisionDate: String?
+ let deprecations: [EntityDeprecation]
+ let supersessions: [EntitySupersession]
+
+// var isDeprecated: Bool { !self.deprecations.isEmpty }
+// var isSuperseded: Bool { !self.supersessions.isEmpty }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.datum = try container.decodeIfPresent(ChildLink.self, forKey: .datum)
+ self.datumEnsemble = try container.decodeIfPresent(ChildLink.self, forKey: .datumEnsemble)
+ self.baseCoordRefSystem = try container.decodeIfPresent(ChildLink.self, forKey: .baseCoordRefSystem)
+ self.conversion = try container.decodeIfPresent(ChildLink.self, forKey: .conversion)
+ self.geoidModels = try container.decodeIfPresent([ChildLink].self, forKey: .geoidModels) ?? []
+ self.usage = try container.decodeIfPresent([Usage].self, forKey: .usage) ?? []
+ self.coordSys = try container.decodeIfPresent(ChildLink.self, forKey: .coordSys)
+ self.kind = try container.decodeIfPresent(String.self, forKey: .kind)
+ self.deformations = try container.decodeIfPresent([ChildLink].self, forKey: .deformations) ?? []
+ self.code = try container.decode(Int.self, forKey: .code)
+ self.changes = try container.decodeIfPresent([ChildLinkBase].self, forKey: .changes) ?? []
+ self.alias = try container.decodeIfPresent([AliasDetails].self, forKey: .alias) ?? []
+ self.links = try container.decodeIfPresent([Link].self, forKey: .links) ?? []
+ self.name = try container.decodeIfPresent(String.self, forKey: .name)
+ self.remark = try container.decodeIfPresent(String.self, forKey: .remark)
+ self.dataSource = try container.decodeIfPresent(String.self, forKey: .dataSource)
+ self.informationSource = try container.decodeIfPresent(String.self, forKey: .informationSource)
+ self.revisionDate = try container.decodeIfPresent(String.self, forKey: .revisionDate)
+ self.deprecations = try container.decodeIfPresent([EntityDeprecation].self, forKey: .deprecations) ?? []
+ self.supersessions = try container.decodeIfPresent([EntitySupersession].self, forKey: .supersessions) ?? []
+ }
+}
+
+struct ChildLink: Decodable {
+ enum CodingKeys: String, CodingKey {
+ case code = "Code"
+ case name = "Name"
+ case href
+ }
+
+ let code: Int
+ let name: String?
+ let href: URL
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.code = try container.decode(Int.self, forKey: .code)
+ self.name = try container.decodeIfPresent(String.self, forKey: .name)
+ self.href = try container.decode(URL.self, forKey: .href)
+ }
+}
+
+struct Usage: Decodable {
+ let Code: Int
+ let Name: String?
+ let ScopeDetails: String?
+ let Scope: ChildLink?
+ let Extent: ChildLink?
+ let Links: [Link]?
+ let Deprecation: [EntityDeprecation]?
+ let Supersession: [EntitySupersession]?
+}
+
+struct ChildLinkBase: Decodable {
+ let Code: Double?
+ let Name: String?
+ let href: String
+}
+
+struct AliasDetails: Decodable {
+ let Code: Int
+ let Alias: String?
+ let NamingSystem: ChildLink?
+ let Remark: String?
+}
+struct EntityDeprecation: Decodable {
+ let Id: Int?
+ let Date: String?
+ let ChangeId: Double?
+ let ReplacedBy: ChildLink?
+ let Reason: String?
+}
+struct EntitySupersession: Decodable {
+ let Id: Int?
+ let SupersededBy: ChildLink?
+ let Year: Int?
+ let Remarks: String?
+ let `Type`: String?
+}
+
+// MARK: - /v1/Datum
+
+struct Datum: Decodable {
+ let `Type`: String?
+ let Origin: String?
+ let PublicationDate: String?
+ let RealizationEpoch: String?
+ let Ellipsoid: ChildLink?
+ let PrimeMeridian: ChildLink?
+ let Usage: [Usage]?
+ let MemberDatums: [ChildLink]?
+ let ConventionalReferenceSystem: ChildLink?
+ let FrameReferenceEpoch: Double?
+ let RealizationMethod: ChildLink?
+ let Code: Int?
+ let Changes: [ChildLinkBase]?
+ let Alias: [AliasDetails]?
+ let Links: [Link]?
+ let Name: String?
+ let Remark: String?
+ let DataSource: String?
+ let InformationSource: String?
+ let RevisionDate: String?
+ let Deprecations: [EntityDeprecation]?
+ let Supersessions: [EntitySupersession]?
+}
+
+struct UnitOfMeasure: Decodable {
+ let `Type`: String?
+ let TargetUnit: ChildLink?
+ let FactorB: Double?
+ let FactorC: Double?
+ let Code: Int
+ let Changes: [ChildLinkBase]?
+ let Alias: [AliasDetails]?
+ let Links: [Link]?
+ let Name: String
+ let Remark: String?
+ let DataSource: String?
+ let InformationSource: String?
+ let RevisionDate: String?
+ let Deprecations: [EntityDeprecation]?
+ let Supersessions: [EntitySupersession]?
+}
diff --git a/Sources/GenerateEPSG/DefaultCaseDecodable.swift b/Sources/GenerateEPSG/DefaultCaseDecodable.swift
new file mode 100644
index 0000000..2f0c39c
--- /dev/null
+++ b/Sources/GenerateEPSG/DefaultCaseDecodable.swift
@@ -0,0 +1,31 @@
+//
+// DefaultCaseDecodable.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/08/2020.
+// Copyright © 2020 Rémi Bardon. All rights reserved.
+//
+
+/// Comes from [OMG a new enum case!](https://link.medium.com/PhETT8BKg9)
+public protocol DefaultCaseDecodable: Decodable, RawRepresentable, CaseIterable
+where RawValue: Equatable & Codable
+{
+ static var defaultCase: Self { get }
+}
+
+public extension DefaultCaseDecodable {
+ static var nonDefaultCases: [Self] {
+ Self.allCases.filter { $0 != Self.defaultCase }
+ }
+
+ init(rawValue: RawValue) {
+ let value = Self.allCases.first { $0.rawValue == rawValue }
+ self = value ?? Self.defaultCase
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let rawValue = try container.decode(RawValue.self)
+ self = Self(rawValue: rawValue) ?? Self.defaultCase
+ }
+}
diff --git a/Sources/GenerateEPSG/Template.swift b/Sources/GenerateEPSG/Template.swift
new file mode 100644
index 0000000..1734879
--- /dev/null
+++ b/Sources/GenerateEPSG/Template.swift
@@ -0,0 +1,47 @@
+//
+// File.swift
+//
+//
+// Created by Rémi Bardon on 10/12/2022.
+//
+
+import Stencil
+
+protocol Template {
+ static var template: String { get }
+ func representation() throws -> String
+}
+
+extension Template {
+ func representation() throws -> String {
+ return try environment.renderTemplate(string: Self.template, context: ["self": self])
+ }
+}
+
+//struct AxisTemplate: Template {
+//
+//}
+//
+//struct CoordinateSystemTemplate: Template {
+// enum Dimensions: String {
+// case two = "TwoDimensionalCS"
+// case three = "ThreeDimensionalCS"
+// }
+//}
+
+struct UnitTemplate: Template {
+ static let template: String = """
+ public enum EPSG{{ self.epsgCode }}: Geodesy.UnitOfMeasurement {
+ public static let epsgName: String = "{{ self.epsgName }}"
+ public static let epsgCode: Int = {{ self.epsgCode }}
+ }
+ """
+
+ let epsgName: String
+ let epsgCode: Int
+
+ init(from dto: UnitOfMeasure) {
+ self.epsgName = dto.Name
+ self.epsgCode = dto.Code
+ }
+}
diff --git a/Sources/GenerateEPSG/UnknownCaseDecodable.swift b/Sources/GenerateEPSG/UnknownCaseDecodable.swift
new file mode 100644
index 0000000..90d2f34
--- /dev/null
+++ b/Sources/GenerateEPSG/UnknownCaseDecodable.swift
@@ -0,0 +1,29 @@
+//
+// UnknownCaseDecodable.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 07/12/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+/// Comes from [OMG a new enum case!](https://link.medium.com/PhETT8BKg9)
+public protocol UnknownCaseDecodable: Decodable, RawRepresentable
+where RawValue: Equatable & Codable
+{
+ static var knownCases: [Self] { get }
+ var isUnknownCase: Bool { get }
+ static func unknownCase(_ rawValue: RawValue) -> Self
+}
+
+public extension UnknownCaseDecodable {
+ init(rawValue: RawValue) {
+ let value = Self.knownCases.first { $0.rawValue == rawValue }
+ self = value ?? Self.unknownCase(rawValue)
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let rawValue = try container.decode(RawValue.self)
+ self = Self(rawValue: rawValue)
+ }
+}
diff --git a/Sources/GenerateEPSG/main.swift b/Sources/GenerateEPSG/main.swift
new file mode 100644
index 0000000..a36408d
--- /dev/null
+++ b/Sources/GenerateEPSG/main.swift
@@ -0,0 +1,112 @@
+//
+// main.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 06/12/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import Foundation
+import Stencil
+
+let environment = Environment()
+
+let session = URLSession.shared
+
+let urlString = "https://apps.epsg.org/api/v1/CoordRefSystem/"
+guard var endpoint = URLComponents(string: urlString) else {
+ exit(EXIT_FAILURE)
+}
+
+let template = """
+{% if crs.name %}\
+/// - Name: {{ crs.name }}
+{% endif %}\
+{% if crs.type %}\
+/// - Type: {{ type }}
+{% endif %}\
+{% if crs.dataSource %}\
+/// - DataSource: {{ crs.dataSource }}
+{% endif %}\
+{% if crs.area %}\
+/// - Area: {{ crs.area }}
+{% endif %}\
+{% if crs.remarks %}\
+/// - Remarks: {{ crs.remarks }}
+{% endif %}\
+/// - Superseded: `{{ crs.supersessions.isEmpty == false }}`
+{% if crs.revisionDate %}\
+/// - RevisionDate: `{{ crs.revisionDate }}`
+{% endif %}\
+{% if crs.deprecated == true %}\
+@available(*, deprecated)
+{% endif %}\
+enum EPSG{{ crs.code }}: {{ swiftType }} {
+
+}
+"""
+
+let api = EPSGAPI()
+
+let decoder = JSONDecoder()
+for i in 0...0 {
+ endpoint.queryItems = [
+ URLQueryItem(name: "page", value: String(describing: i)),
+ ]
+ guard let url: URL = endpoint.url else { continue }
+ let results = try await api.get(SearchResults.self, from: url)
+
+ for result in results.results {
+ guard result.type.isSupported else {
+ print("CRS type \"\(result.type.rawValue)\" not supported")
+ continue
+ }
+
+ if let url = result.links.first?.href {
+ let crs: GeographicCoordRefSystem = try await api.get(GeographicCoordRefSystem.self, from: url)
+ print(crs)
+
+ guard let datumUrl = crs.datum?.href else {
+ print("CRS `\(crs.code)` has no datum: \(crs)")
+ continue
+ }
+ let datum = try await api.get(Datum.self, from: datumUrl)
+ print(datum)
+
+ let context: [String : Any] = [
+ "type": result.type.rawValue,
+ "swiftType": result.type.swiftType,
+ "crs": crs,
+ "datum": datum,
+ ]
+ let code = try environment.renderTemplate(string: template, context: context)
+ print(code)
+ }
+ }
+}
+
+actor EPSGAPI {
+ private var cache = [URL: Any]()
+
+ func get(_ type: T.Type, from url: URL) async throws -> T {
+ if let res = cache[url] as? T {
+ print("🎯 Cache hit: \(url)")
+ return res
+ }
+
+ let (data, response) = try await session.data(from: url)
+ guard let response = response as? HTTPURLResponse else {
+ throw URLError(.badServerResponse)
+ }
+ guard (200..<300).contains(response.statusCode) else {
+ throw URLError(.badServerResponse)
+ }
+ let res = try decoder.decode(type, from: data)
+
+ cache[url] = res
+ return res
+ }
+}
+
+// curl -X GET --header 'Accept: application/json' 'https://apps.epsg.org/api/v1/GeodeticCoordRefSystem/4168'
+//{"Datum":{"Code":6168,"Name":"Accra","href":"https://apps.epsg.org/api/v1/Datum/6168"},"DatumEnsemble":null,"BaseCoordRefSystem":null,"Conversion":null,"GeoidModels":[],"Usage":[{"Code":3053,"Name":"Ghana","ScopeDetails":"Geodesy.","Scope":{"Code":1027,"Name":"Geodesy.","href":"https://apps.epsg.org/api/v1/Scope/1027"},"Extent":{"Code":1104,"Name":"Ghana","href":"https://apps.epsg.org/api/v1/Extent/1104"},"Links":[],"Deprecation":null,"Supersession":null}],"CoordSys":{"Code":6422,"Name":"Ellipsoidal 2D CS. Axes: latitude, longitude. Orientations: north, east. UoM: degree","href":"https://apps.epsg.org/api/v1/CoordSystem/6422"},"Kind":"geographic 2D","Deformations":[],"Code":4168,"Changes":[{"Code":2003.37,"href":"https://apps.epsg.org/api/v1/Change/2003.37"}],"Alias":[],"Links":[{"rel":"self","href":"https://apps.epsg.org/api/v1/GeodeticCoordRefSystem/4168"}],"Name":"Accra","Remark":"Ellipsoid semi-major axis (a)=20926201 exactly Gold Coast feet. \r\nReplaced by Leigon (code 4250) in 1978.","DataSource":"EPSG","InformationSource":"Ordnance Survey International","RevisionDate":"2004-01-06T00:00:00","Deprecations":[],"Supersessions":[]}