From af19999b7ade3a078023861e7d010208f9f3a3bf Mon Sep 17 00:00:00 2001 From: Fredrik Karlsson Date: Wed, 19 Sep 2018 15:19:28 +0200 Subject: [PATCH] Expose Placemark.properties Also refactored GeocodedPlacemark out of Placemark to improve readability. --- MapboxGeocoder.xcodeproj/project.pbxproj | 18 ++ MapboxGeocoder/MBGeocodedPlacemark.swift | 194 +++++++++++++++ MapboxGeocoder/MBPlacemark.swift | 230 +++--------------- .../ForwardGeocodingTests.swift | 46 +++- .../fixtures/forward_valid_hk.json | 1 + 5 files changed, 287 insertions(+), 202 deletions(-) create mode 100644 MapboxGeocoder/MBGeocodedPlacemark.swift create mode 100644 MapboxGeocoderTests/fixtures/forward_valid_hk.json diff --git a/MapboxGeocoder.xcodeproj/project.pbxproj b/MapboxGeocoder.xcodeproj/project.pbxproj index 4c9bbb6..01a9d93 100644 --- a/MapboxGeocoder.xcodeproj/project.pbxproj +++ b/MapboxGeocoder.xcodeproj/project.pbxproj @@ -46,6 +46,13 @@ 35506B8B200F856400629509 /* BridgingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 35506B8A200F856400629509 /* BridgingTests.m */; }; 35506B8C200F856400629509 /* BridgingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 35506B8A200F856400629509 /* BridgingTests.m */; }; 35506B8D200F856400629509 /* BridgingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 35506B8A200F856400629509 /* BridgingTests.m */; }; + 355A7B7C21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 355A7B7B21527B700001D2AD /* MBGeocodedPlacemark.swift */; }; + 355A7B7D21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 355A7B7B21527B700001D2AD /* MBGeocodedPlacemark.swift */; }; + 355A7B7E21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 355A7B7B21527B700001D2AD /* MBGeocodedPlacemark.swift */; }; + 355A7B7F21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 355A7B7B21527B700001D2AD /* MBGeocodedPlacemark.swift */; }; + 355A7B8121527FAF0001D2AD /* forward_valid_hk.json in Resources */ = {isa = PBXBuildFile; fileRef = 355A7B8021527FAF0001D2AD /* forward_valid_hk.json */; }; + 355A7B8221527FAF0001D2AD /* forward_valid_hk.json in Resources */ = {isa = PBXBuildFile; fileRef = 355A7B8021527FAF0001D2AD /* forward_valid_hk.json */; }; + 355A7B8321527FAF0001D2AD /* forward_valid_hk.json in Resources */ = {isa = PBXBuildFile; fileRef = 355A7B8021527FAF0001D2AD /* forward_valid_hk.json */; }; 357B4358202CC90A00735521 /* reverse_address.json in Resources */ = {isa = PBXBuildFile; fileRef = 357B4357202CC90A00735521 /* reverse_address.json */; }; 357B4359202CC90A00735521 /* reverse_address.json in Resources */ = {isa = PBXBuildFile; fileRef = 357B4357202CC90A00735521 /* reverse_address.json */; }; 357B435A202CC90A00735521 /* reverse_address.json in Resources */ = {isa = PBXBuildFile; fileRef = 357B4357202CC90A00735521 /* reverse_address.json */; }; @@ -223,6 +230,8 @@ 07CF85C220F6DF93007B26B6 /* permanent_reverse_multiple_no_results.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = permanent_reverse_multiple_no_results.json; sourceTree = ""; }; 07CF85C620F6DFEB007B26B6 /* permanent_reverse_single_valid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = permanent_reverse_single_valid.json; sourceTree = ""; }; 35506B8A200F856400629509 /* BridgingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BridgingTests.m; sourceTree = ""; }; + 355A7B7B21527B700001D2AD /* MBGeocodedPlacemark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBGeocodedPlacemark.swift; sourceTree = ""; }; + 355A7B8021527FAF0001D2AD /* forward_valid_hk.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = forward_valid_hk.json; sourceTree = ""; }; 357B4357202CC90A00735521 /* reverse_address.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = reverse_address.json; sourceTree = ""; }; 35D3DE382112410A00B62912 /* CoreLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreLocation.swift; sourceTree = ""; }; DA1AC0211E5C23B8006DF1D6 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; @@ -415,6 +424,7 @@ DA2EC05D1CED732F00D4BA5D /* MBGeocodeOptions.swift */, DDC229591A36073B006BE405 /* MBGeocoder.swift */, DA2E03EF1CB0FDB400D1269A /* MBPlacemark.swift */, + 355A7B7B21527B700001D2AD /* MBGeocodedPlacemark.swift */, DA29C8DE1CEBE90200E48A61 /* MBPlacemarkScope.h */, DA2EC05B1CED72E900D4BA5D /* MBPlacemarkScope.swift */, DA2E03F11CB0FE0200D1269A /* MBRectangularRegion.swift */, @@ -488,6 +498,7 @@ DDF1E85B1BD70E4C00C40C78 /* reverse_valid.json */, DDF1E85A1BD70E4C00C40C78 /* reverse_invalid.json */, 357B4357202CC90A00735521 /* reverse_address.json */, + 355A7B8021527FAF0001D2AD /* forward_valid_hk.json */, ); name = Fixtures; path = fixtures; @@ -793,6 +804,7 @@ buildActionMask = 2147483647; files = ( 07CF85B020F6DF2F007B26B6 /* permanent_forward_multiple_no_results.json in Resources */, + 355A7B8221527FAF0001D2AD /* forward_valid_hk.json in Resources */, 07CF85A820F6DF0F007B26B6 /* permanent_invalid.json in Resources */, 07CF85C020F6DF86007B26B6 /* permanent_reverse_single_no_results.json in Resources */, DA5170B81CF1B1F900CD6DCF /* forward_valid.json in Resources */, @@ -824,6 +836,7 @@ buildActionMask = 2147483647; files = ( 07CF85B120F6DF2F007B26B6 /* permanent_forward_multiple_no_results.json in Resources */, + 355A7B8321527FAF0001D2AD /* forward_valid_hk.json in Resources */, 07CF85A920F6DF0F007B26B6 /* permanent_invalid.json in Resources */, 07CF85C120F6DF86007B26B6 /* permanent_reverse_single_no_results.json in Resources */, DA5170E21CF2542B00CD6DCF /* forward_valid.json in Resources */, @@ -878,6 +891,7 @@ buildActionMask = 2147483647; files = ( 07CF85AF20F6DF2F007B26B6 /* permanent_forward_multiple_no_results.json in Resources */, + 355A7B8121527FAF0001D2AD /* forward_valid_hk.json in Resources */, 07CF85A720F6DF0F007B26B6 /* permanent_invalid.json in Resources */, 07CF85BF20F6DF86007B26B6 /* permanent_reverse_single_no_results.json in Resources */, DA210BAD1CB4BFF7008088FD /* forward_valid.json in Resources */, @@ -970,6 +984,7 @@ files = ( DA5170B31CF1B1E000CD6DCF /* MBRectangularRegion.swift in Sources */, DA5170B01CF1B1E000CD6DCF /* MBPlacemark.swift in Sources */, + 355A7B7D21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */, DA5170AF1CF1B1E000CD6DCF /* MBGeocoder.swift in Sources */, DA5170B21CF1B1E000CD6DCF /* MBPlacemarkScope.swift in Sources */, DA5170AE1CF1B1E000CD6DCF /* MBGeocodeOptions.swift in Sources */, @@ -996,6 +1011,7 @@ files = ( DA5170DE1CF2541C00CD6DCF /* MBRectangularRegion.swift in Sources */, DA5170DB1CF2541C00CD6DCF /* MBPlacemark.swift in Sources */, + 355A7B7E21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */, DA5170DA1CF2541C00CD6DCF /* MBGeocoder.swift in Sources */, DA5170DD1CF2541C00CD6DCF /* MBPlacemarkScope.swift in Sources */, DA5170D91CF2541C00CD6DCF /* MBGeocodeOptions.swift in Sources */, @@ -1022,6 +1038,7 @@ files = ( DA5170F91CF2582F00CD6DCF /* MBRectangularRegion.swift in Sources */, DA5170F61CF2582F00CD6DCF /* MBPlacemark.swift in Sources */, + 355A7B7F21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */, DA5170F51CF2582F00CD6DCF /* MBGeocoder.swift in Sources */, DA5170F81CF2582F00CD6DCF /* MBPlacemarkScope.swift in Sources */, DA5170F41CF2582F00CD6DCF /* MBGeocodeOptions.swift in Sources */, @@ -1044,6 +1061,7 @@ files = ( DA2EC05E1CED732F00D4BA5D /* MBGeocodeOptions.swift in Sources */, DA2E03F21CB0FE0200D1269A /* MBRectangularRegion.swift in Sources */, + 355A7B7C21527B700001D2AD /* MBGeocodedPlacemark.swift in Sources */, DDC2295E1A360843006BE405 /* MBGeocoder.swift in Sources */, DA2EC05C1CED72E900D4BA5D /* MBPlacemarkScope.swift in Sources */, DA2E03F01CB0FDB400D1269A /* MBPlacemark.swift in Sources */, diff --git a/MapboxGeocoder/MBGeocodedPlacemark.swift b/MapboxGeocoder/MBGeocodedPlacemark.swift new file mode 100644 index 0000000..a67b5e9 --- /dev/null +++ b/MapboxGeocoder/MBGeocodedPlacemark.swift @@ -0,0 +1,194 @@ +import Foundation + + +/** + A concrete subclass of `Placemark` to represent results of geocoding requests. + */ +@objc(MBGeocodedPlacemark) +open class GeocodedPlacemark: Placemark { + + private enum CodingKeys: String, CodingKey { + case routableLocations = "routable_points" + case relevance + } + + private enum PointsCodingKeys: String, CodingKey { + case points + } + + /** + An array of locations that serve as hints for navigating to the placemark. + + If the `GeocodeOptions.includesRoutableLocations` property is set to `true`, this property contains locations that are suitable to use as a waypoint in a routing engine such as MapboxDirections.swift. Otherwise, if the `GeocodeOptions.includesRoutableLocations` property is set to `false`, this property is set to `nil`. + + For the placemark’s geographic center, use the `location` property. The routable locations may differ from the geographic center. For example, if a house’s driveway leads to a street other than the nearest street (by straight-line distance), then this property may contain the location where the driveway meets the street. A route to the placemark’s geographic center may be impassable, but a route to the routable location would end on the correct street with access to the house. + */ + @objc open var routableLocations: [CLLocation]? + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let pointsContainer = try? container.nestedContainer(keyedBy: PointsCodingKeys.self, forKey: .routableLocations), + var coordinatesContainer = try? pointsContainer.nestedUnkeyedContainer(forKey: .points) { + + if let routableLocation = try coordinatesContainer.decodeIfPresent(RoutableLocation.self), + let coordinate = routableLocation.coordinate { + routableLocations = [CLLocation(coordinate: coordinate)] + } + } + + relevance = try container.decodeIfPresent(Double.self, forKey: .relevance) ?? -1 + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(relevance, forKey: .relevance) + + if let routableLocations = routableLocations, + !routableLocations.isEmpty { + var pointsContainer = container.nestedContainer(keyedBy: PointsCodingKeys.self, forKey: .routableLocations) + var coordinatesContainer = pointsContainer.nestedUnkeyedContainer(forKey: .points) + let routableLocation = RoutableLocation(coordinates: [routableLocations[0].coordinate.longitude, + routableLocations[0].coordinate.latitude]) + try coordinatesContainer.encode(routableLocation) + } + + try super.encode(to: encoder) + } + + @objc open override var debugDescription: String { + return qualifiedName! + } + + internal var qualifiedNameComponents: [String] { + if qualifiedName!.contains(", ") { + return qualifiedName!.components(separatedBy: ", ") + } + // Chinese addresses have no commas and are reversed. + return (superiorPlacemarks?.map { $0.name } ?? []).reversed() + [name] + } + + @objc open var formattedName: String { + let text = super.name + // For address features, `text` is just the street name. Look through the fully-qualified address to determine whether to put the house number before or after the street name. + if let houseNumber = address, scope == .address { + let streetName = text + let reversedAddress = "\(streetName) \(houseNumber)" + if qualifiedNameComponents.contains(reversedAddress) { + return reversedAddress + } else { + return "\(houseNumber) \(streetName)" + } + } else { + return text + } + } + + @objc open override var genres: [String]? { + return properties?.category?.components(separatedBy: ", ") + } + + @objc open override var imageName: String? { + return properties?.maki + } + + /** + A numerical score from 0 (least relevant) to 0.99 (most relevant) measuring + how well each returned feature matches the query. Use this property to + remove results that don’t fully match the query. + */ + @objc open var relevance: Double + + private var clippedAddressLines: [String] { + let lines = qualifiedNameComponents + if scope == .address { + return lines + } + + guard let qualifiedName = qualifiedName, + qualifiedName.contains(", ") else { + // Chinese addresses have no commas and are reversed. + return Array(lines.prefix(lines.count)) + } + + return Array(lines.suffix(from: 1)) + } + + /** + The placemark’s full address in the customary local format, with each line in a separate string in the array. + + If you need to fit the same address on a single line, use the `qualifiedName` property, in which each line is separated by a comma instead of a line break. + */ + var formattedAddressLines: [String]? { + return clippedAddressLines + } + + #if !os(tvOS) + @available(iOS 9.0, OSX 10.11, *) + @objc open override var postalAddress: CNPostalAddress? { + let postalAddress = CNMutablePostalAddress() + + if scope == .address { + postalAddress.street = name + } else if let address = address { + postalAddress.street = address.replacingOccurrences(of: ", ", with: "\n") + } + + if let placeName = place?.name { + postalAddress.city = placeName + } + if let regionName = administrativeRegion?.name { + postalAddress.state = regionName + } + if let postalCode = postalCode?.name { + postalAddress.postalCode = postalCode + } + if let countryName = country?.name { + postalAddress.country = countryName + } + if let ISOCountryCode = country?.code { + postalAddress.isoCountryCode = ISOCountryCode + } + + return postalAddress + } + #endif + + open override var code: String? { + get { return country?.code } + set { country?.code = code } + } + + @objc open override var addressDictionary: [AnyHashable: Any]? { + var addressDictionary: [String: Any] = [:] + if scope == .address { + addressDictionary[MBPostalAddressStreetKey] = name + } else if let address = properties?.address { + addressDictionary[MBPostalAddressStreetKey] = address + } else if let address = address { + addressDictionary[MBPostalAddressStreetKey] = address + } + addressDictionary[MBPostalAddressCityKey] = place?.name + addressDictionary[MBPostalAddressStateKey] = administrativeRegion?.name + addressDictionary[MBPostalAddressPostalCodeKey] = postalCode?.name + addressDictionary[MBPostalAddressCountryKey] = country?.name + addressDictionary[MBPostalAddressISOCountryCodeKey] = country?.code + addressDictionary["formattedAddressLines"] = clippedAddressLines + addressDictionary["name"] = name + addressDictionary["subAdministrativeArea"] = district?.name ?? place?.name + addressDictionary["subLocality"] = neighborhood?.name + addressDictionary["subThoroughfare"] = subThoroughfare + addressDictionary["thoroughfare"] = thoroughfare + return addressDictionary + } + + /** + The phone number to contact a business at this location. + */ + @objc open override var phoneNumber: String? { + return properties?.phoneNumber + } +} diff --git a/MapboxGeocoder/MBPlacemark.swift b/MapboxGeocoder/MBPlacemark.swift index 64f6694..b6f870b 100644 --- a/MapboxGeocoder/MBPlacemark.swift +++ b/MapboxGeocoder/MBPlacemark.swift @@ -87,14 +87,13 @@ open class Placemark: NSObject, Codable { name = try container.decode(String.self, forKey: .name) address = try container.decodeIfPresent(String.self, forKey: .address) qualifiedName = try container.decodeIfPresent(String.self, forKey: .qualifiedName) - superiorPlacemarks = try container.decodeIfPresent([GeocodedPlacemark].self, forKey: .superiorPlacemarks) + superiorPlacemarks = try container.decodeIfPresent([Placemark].self, forKey: .superiorPlacemarks) if let coordinates = try container.decodeIfPresent([CLLocationDegrees].self, forKey: .centerCoordinate) { let coordinate = CLLocationCoordinate2D(geoJSON: coordinates) location = CLLocation(coordinate: coordinate) } - code = try container.decodeIfPresent(String.self, forKey: .code)?.uppercased() if let rawIdentifier = try container.decodeIfPresent(String.self, forKey: .wikidataItemIdentifier) { let identifier = rawIdentifier.trimmingCharacters(in: .whitespacesAndNewlines) assert(identifier.hasPrefix("Q")) @@ -102,6 +101,7 @@ open class Placemark: NSObject, Codable { } properties = try container.decodeIfPresent(Properties.self, forKey: .properties) + code = try container.decodeIfPresent(String.self, forKey: .code)?.uppercased() if let boundingBox = try container.decodeIfPresent([CLLocationDegrees].self, forKey: .boundingBox) { let southWest = CLLocationCoordinate2D(geoJSON: Array(boundingBox.prefix(2))) @@ -152,7 +152,7 @@ open class Placemark: NSObject, Codable { /** A subset of the `properties` object on a GeoJSON feature suited for Geocoding results. */ - fileprivate var properties: Properties? + public var properties: Properties? /** The common name of the placemark. @@ -229,15 +229,6 @@ open class Placemark: NSObject, Codable { // MARK: Accessing Contact Information - /** - The placemark’s full address in the customary local format, with each line in a separate string in the array. - - If you need to fit the same address on a single line, use the `qualifiedName` property, in which each line is separated by a comma instead of a line break. - */ - fileprivate var formattedAddressLines: [String]? { - return nil - } - #if !os(tvOS) /** The placemark’s postal address. @@ -367,22 +358,41 @@ internal struct GeocodeResult: Codable { } /** - A subset of the `properties` object on a GeoJSON feature suited for Geocoding results. + An object describing the placemark. Only Carmen (https://github.com/mapbox/carmen) properties are guaranteed. */ -internal struct Properties: Codable { +public class Properties: Codable { private enum CodingKeys: String, CodingKey { case shortCode = "short_code" case phoneNumber = "tel" case maki case address case category + case landmark + case wikidata } - let shortCode: String? - let maki: String? - let phoneNumber: String? - let address: String? - let category: String? + // The ISO 3166-1 country and ISO 3166-2 region code for the returned feature. + public let shortCode: String? + + // The name of a suggested Maki icon (https://www.mapbox.com/maki-icons/) to visualize a poi feature based on its category. + public let maki: String? + + // A formatted string of the telephone number for the returned poi feature. + public let phoneNumber: String? + + // A string of the full street address for the returned poi feature. + // Note that unlike the address property for address features, this property is inside the properties object. + public let address: String? + + // A boolean value indicating whether a poi feature is a landmark. + // Landmarks are particularly notable or long-lived features like schools, parks, museums and places of worship. + public let landmark: Bool? + + // A string of comma-separated categories for the returned poi feature. + public let category: String? + + // The Wikidata identifier for the returned feature. + public let wikidata: String? } // Used internally for flattening and transforming routable_points.points.coordinates @@ -397,188 +407,6 @@ internal struct RoutableLocation: Codable { } } -/** - A concrete subclass of `Placemark` to represent results of geocoding requests. - */ -@objc(MBGeocodedPlacemark) -open class GeocodedPlacemark: Placemark { - - private enum CodingKeys: String, CodingKey { - case routableLocations = "routable_points" - case relevance - } - - private enum PointsCodingKeys: String, CodingKey { - case points - } - - /** - An array of locations that serve as hints for navigating to the placemark. - - If the `GeocodeOptions.includesRoutableLocations` property is set to `true`, this property contains locations that are suitable to use as a waypoint in a routing engine such as MapboxDirections.swift. Otherwise, if the `GeocodeOptions.includesRoutableLocations` property is set to `false`, this property is set to `nil`. - - For the placemark’s geographic center, use the `location` property. The routable locations may differ from the geographic center. For example, if a house’s driveway leads to a street other than the nearest street (by straight-line distance), then this property may contain the location where the driveway meets the street. A route to the placemark’s geographic center may be impassable, but a route to the routable location would end on the correct street with access to the house. - */ - @objc open var routableLocations: [CLLocation]? - - public required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - if let pointsContainer = try? container.nestedContainer(keyedBy: PointsCodingKeys.self, forKey: .routableLocations), - var coordinatesContainer = try? pointsContainer.nestedUnkeyedContainer(forKey: .points) { - - if let routableLocation = try coordinatesContainer.decodeIfPresent(RoutableLocation.self), - let coordinate = routableLocation.coordinate { - routableLocations = [CLLocation(coordinate: coordinate)] - } - } - - relevance = try container.decodeIfPresent(Double.self, forKey: .relevance) ?? -1 - - try super.init(from: decoder) - } - - public override func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(relevance, forKey: .relevance) - - if let routableLocations = routableLocations, - !routableLocations.isEmpty { - var pointsContainer = container.nestedContainer(keyedBy: PointsCodingKeys.self, forKey: .routableLocations) - var coordinatesContainer = pointsContainer.nestedUnkeyedContainer(forKey: .points) - let routableLocation = RoutableLocation(coordinates: [routableLocations[0].coordinate.longitude, - routableLocations[0].coordinate.latitude]) - try coordinatesContainer.encode(routableLocation) - } - - try super.encode(to: encoder) - } - - @objc open override var debugDescription: String { - return qualifiedName! - } - - internal var qualifiedNameComponents: [String] { - if qualifiedName!.contains(", ") { - return qualifiedName!.components(separatedBy: ", ") - } - // Chinese addresses have no commas and are reversed. - return (superiorPlacemarks?.map { $0.name } ?? []).reversed() + [name] - } - - @objc open var formattedName: String { - let text = super.name - // For address features, `text` is just the street name. Look through the fully-qualified address to determine whether to put the house number before or after the street name. - if let houseNumber = address, scope == .address { - let streetName = text - let reversedAddress = "\(streetName) \(houseNumber)" - if qualifiedNameComponents.contains(reversedAddress) { - return reversedAddress - } else { - return "\(houseNumber) \(streetName)" - } - } else { - return text - } - } - - @objc open override var genres: [String]? { - return properties?.category?.components(separatedBy: ", ") - } - - @objc open override var imageName: String? { - return properties?.maki - } - - /** - A numerical score from 0 (least relevant) to 0.99 (most relevant) measuring - how well each returned feature matches the query. Use this property to - remove results that don’t fully match the query. - */ - @objc open var relevance: Double - - private var clippedAddressLines: [String] { - let lines = qualifiedNameComponents - if scope == .address { - return lines - } - - guard let qualifiedName = qualifiedName, - qualifiedName.contains(", ") else { - // Chinese addresses have no commas and are reversed. - return Array(lines.prefix(lines.count)) - } - - return Array(lines.suffix(from: 1)) - } - - override var formattedAddressLines: [String] { - return clippedAddressLines - } - - #if !os(tvOS) - @available(iOS 9.0, OSX 10.11, *) - @objc open override var postalAddress: CNPostalAddress? { - let postalAddress = CNMutablePostalAddress() - - if scope == .address { - postalAddress.street = name - } else if let address = address { - postalAddress.street = address.replacingOccurrences(of: ", ", with: "\n") - } - - if let placeName = place?.name { - postalAddress.city = placeName - } - if let regionName = administrativeRegion?.name { - postalAddress.state = regionName - } - if let postalCode = postalCode?.name { - postalAddress.postalCode = postalCode - } - if let countryName = country?.name { - postalAddress.country = countryName - } - if let ISOCountryCode = country?.code { - postalAddress.isoCountryCode = ISOCountryCode - } - - return postalAddress - } - #endif - - @objc open override var addressDictionary: [AnyHashable: Any]? { - var addressDictionary: [String: Any] = [:] - if scope == .address { - addressDictionary[MBPostalAddressStreetKey] = name - } else if let address = properties?.address { - addressDictionary[MBPostalAddressStreetKey] = address - } else if let address = address { - addressDictionary[MBPostalAddressStreetKey] = address - } - addressDictionary[MBPostalAddressCityKey] = place?.name - addressDictionary[MBPostalAddressStateKey] = administrativeRegion?.name - addressDictionary[MBPostalAddressPostalCodeKey] = postalCode?.name - addressDictionary[MBPostalAddressCountryKey] = country?.name - addressDictionary[MBPostalAddressISOCountryCodeKey] = country?.code - addressDictionary["formattedAddressLines"] = clippedAddressLines - addressDictionary["name"] = name - addressDictionary["subAdministrativeArea"] = district?.name ?? place?.name - addressDictionary["subLocality"] = neighborhood?.name - addressDictionary["subThoroughfare"] = subThoroughfare - addressDictionary["thoroughfare"] = thoroughfare - return addressDictionary - } - - /** - The phone number to contact a business at this location. - */ - @objc open override var phoneNumber: String? { - return properties?.phoneNumber - } -} - /** A concrete subclass of `Placemark` to represent entries in a `GeocodedPlacemark` object’s `superiorPlacemarks` property. These entries are like top-level geocoding results, except that they lack location information and are flatter, with properties directly at the top level. */ diff --git a/MapboxGeocoderTests/ForwardGeocodingTests.swift b/MapboxGeocoderTests/ForwardGeocodingTests.swift index 5e38bac..9cf3207 100644 --- a/MapboxGeocoderTests/ForwardGeocodingTests.swift +++ b/MapboxGeocoderTests/ForwardGeocodingTests.swift @@ -109,13 +109,14 @@ class ForwardGeocodingTests: XCTestCase { let geocoder = Geocoder(accessToken: BogusToken) var placemark: GeocodedPlacemark! = nil + var secondPlacemark: GeocodedPlacemark! = nil let options = ForwardGeocodeOptions(query: "hainan") options.allowedISOCountryCodes = ["CN"] options.locale = Locale(identifier: "zh-Hans") let task = geocoder.geocode(options) { (placemarks, attribution, error) in XCTAssertEqual(placemarks?.count, 3) placemark = placemarks![0] - + secondPlacemark = placemarks![1] XCTAssertEqual(attribution, "NOTICE: © 2016 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.") expectation.fulfill() @@ -153,5 +154,48 @@ class ForwardGeocodingTests: XCTestCase { XCTAssertEqual(addressDictionary[MBPostalAddressStateKey] as? String, "内蒙古", "forward geocode should populate state in address dictionary") XCTAssertEqual(addressDictionary[MBPostalAddressCountryKey] as? String, "中国", "forward geocode should populate country in address dictionary") XCTAssertEqual(addressDictionary[MBPostalAddressISOCountryCodeKey] as? String, "CN", "forward geocode should populate ISO country code in address dictionary") + + XCTAssertEqual(secondPlacemark.properties?.shortCode, "CN-46") + XCTAssertEqual(secondPlacemark.code, "CN") + } + + func testValidHongKongForwardGeocode() { + let expectation = self.expectation(description: "forward geocode should return results") + + _ = stub(condition: isHost("api.mapbox.com") + && isPath("/geocoding/v5/mapbox.places/hainan.json") + && containsQueryParams(["country": "cn", "language": "zh", "access_token": BogusToken])) { _ in + let path = Bundle(for: type(of: self)).path(forResource: "forward_valid_hk", ofType: "json") + return OHHTTPStubsResponse(fileAtPath: path!, statusCode: 200, headers: ["Content-Type": "application/vnd.geo+json"]) + } + + let geocoder = Geocoder(accessToken: BogusToken) + var placemark: GeocodedPlacemark! = nil + var fourthPlacemark: GeocodedPlacemark! = nil + let options = ForwardGeocodeOptions(query: "hainan") + options.allowedISOCountryCodes = ["CN"] + options.locale = Locale(identifier: "zh-Hans") + let task = geocoder.geocode(options) { (placemarks, attribution, error) in + XCTAssertEqual(placemarks?.count, 6) + placemark = placemarks![0] + fourthPlacemark = placemarks![3] + XCTAssertEqual(attribution, "NOTICE: © 2018 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.") + + expectation.fulfill() + } + XCTAssertNotNil(task) + + waitForExpectations(timeout: 1) { (error) in + XCTAssertNil(error, "Error: \(error!)") + XCTAssertEqual(task.state, .completed) + } + + XCTAssertEqual(placemark.properties?.wikidata, "Q8646") + XCTAssertEqual(fourthPlacemark.properties?.phoneNumber, "2732 3232") + XCTAssertEqual(fourthPlacemark.properties?.address, "2 Science Museum Rd") + XCTAssertEqual(fourthPlacemark.properties?.category, "museum") + XCTAssertEqual(fourthPlacemark.properties?.landmark, true) + XCTAssertEqual(fourthPlacemark.properties?.wikidata, "Q836319") + XCTAssertEqual(fourthPlacemark.properties?.maki, "museum") } } diff --git a/MapboxGeocoderTests/fixtures/forward_valid_hk.json b/MapboxGeocoderTests/fixtures/forward_valid_hk.json new file mode 100644 index 0000000..5630114 --- /dev/null +++ b/MapboxGeocoderTests/fixtures/forward_valid_hk.json @@ -0,0 +1 @@ +{"type":"FeatureCollection","query":["hong","kong"],"features":[{"id":"place.364","type":"Feature","place_type":["country","region","place"],"relevance":1,"properties":{"wikidata":"Q8646","short_code":"hk"},"text":"Hong Kong","place_name":"Hong Kong","bbox":[113.7734944,22.134324,114.5045213,22.573549],"center":[114.135442,22.346578],"geometry":{"type":"Point","coordinates":[114.135442,22.346578]}},{"id":"place.12997319628474470","type":"Feature","place_type":["place"],"relevance":1,"properties":{"wikidata":"Q8646"},"text":"Hong Kong","place_name":"Hong Kong, Hong Kong","bbox":[114.080826,22.198214,114.249146,22.321415],"center":[114.164054,22.25959],"geometry":{"type":"Point","coordinates":[114.164054,22.25959]},"context":[{"id":"region.364","short_code":"hk","wikidata":"Q8646","text":"Hong Kong"}]},{"id":"poi.13251169","type":"Feature","place_type":["poi"],"relevance":1,"properties":{"tel":"2721 0116","address":"10 Salisbury Rd","category":"museum","landmark":true,"wikidata":"Q908216","maki":"museum"},"text":"Hong Kong Museum of Art","place_name":"Hong Kong Museum of Art, 10 Salisbury Rd, Kowloon, Hong Kong","center":[114.17208,22.293325],"geometry":{"type":"Point","coordinates":[114.17208,22.293325]},"context":[{"id":"locality.4068150428060122","wikidata":"Q157669","text":"Youjianwang Qu"},{"id":"place.8363024200553460","wikidata":"Q239143","text":"Kowloon"},{"id":"region.364","short_code":"hk","wikidata":"Q8646","text":"Hong Kong"}]},{"id":"poi.13242214","type":"Feature","place_type":["poi"],"relevance":1,"properties":{"tel":"2732 3232","address":"2 Science Museum Rd","category":"museum","landmark":true,"wikidata":"Q836319","maki":"museum"},"text":"Hong Kong Science Museum","place_name":"Hong Kong Science Museum, 2 Science Museum Rd, Kowloon, Hong Kong","center":[114.17726,22.301245],"geometry":{"type":"Point","coordinates":[114.17726,22.301245]},"context":[{"id":"locality.4068150428060122","wikidata":"Q157669","text":"Youjianwang Qu"},{"id":"place.8363024200553460","wikidata":"Q239143","text":"Kowloon"},{"id":"region.364","short_code":"hk","wikidata":"Q8646","text":"Hong Kong"}]},{"id":"poi.13245629","type":"Feature","place_type":["poi"],"relevance":1,"properties":{"tel":"2355 7234","address":"9 Cheong Wan Rd","category":"music, show venue, concert, concert hall","landmark":true,"wikidata":"Q835531","maki":"music"},"text":"Hong Kong Coliseum","place_name":"Hong Kong Coliseum, 9 Cheong Wan Rd, Kowloon, Hong Kong","center":[114.18203,22.301416],"geometry":{"type":"Point","coordinates":[114.18203,22.301416]},"context":[{"id":"locality.4068150428060122","wikidata":"Q157669","text":"Youjianwang Qu"},{"id":"place.8363024200553460","wikidata":"Q239143","text":"Kowloon"},{"id":"region.364","short_code":"hk","wikidata":"Q8646","text":"Hong Kong"}]},{"id":"poi.2411473","type":"Feature","place_type":["poi"],"relevance":0.99,"properties":{"address":"16th St","category":"stadium, arena","landmark":true,"wikidata":"Q7440659","maki":"baseball"},"text":"Seals Stadium","place_name":"Seals Stadium, 16th St, San Francisco, California 94103, United States","matching_text":"stadium","matching_place_name":"stadium, 16th St, San Francisco, California 94103, United States","center":[-122.41035,37.76573],"geometry":{"type":"Point","coordinates":[-122.41035,37.76573]},"context":[{"id":"neighborhood.293934","text":"Mission"},{"id":"postcode.13892342768265050","text":"94103"},{"id":"place.15734669613361910","wikidata":"Q62","text":"San Francisco"},{"id":"region.3591","short_code":"US-CA","wikidata":"Q99","text":"California"},{"id":"country.3145","short_code":"us","wikidata":"Q30","text":"United States"}]}],"attribution":"NOTICE: © 2018 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained."}