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":[]}