diff --git a/platform/ios/MapLibre.docc/LineStyleLayerExampleUIKit.md b/platform/ios/MapLibre.docc/LineStyleLayerExampleUIKit.md index 8ad3d9159..6285c373d 100644 --- a/platform/ios/MapLibre.docc/LineStyleLayerExampleUIKit.md +++ b/platform/ios/MapLibre.docc/LineStyleLayerExampleUIKit.md @@ -71,8 +71,7 @@ class LineStyleLayerExampleUIKit: UIViewController, MLNMapViewDelegate { layer.lineColor = NSExpression(forConstantValue: UIColor(red: 59 / 255, green: 178 / 255, blue: 208 / 255, alpha: 1)) // Use `NSExpression` to smoothly adjust the line width from 2pt to 20pt between zoom levels 14 and 18. The `interpolationBase` parameter allows the values to interpolate along an exponential curve. - layer.lineWidth = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", - [14: 2, 18: 20]) + layer.lineWidth = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [14: 2, 18: 20])) // We can also add a second layer that will draw a stroke around the original line. let casingLayer = MLNLineStyleLayer(identifier: "polyline-case", source: source) @@ -84,7 +83,7 @@ class LineStyleLayerExampleUIKit: UIViewController, MLNMapViewDelegate { // Stroke color slightly darker than the line color. casingLayer.lineColor = NSExpression(forConstantValue: UIColor(red: 41 / 255, green: 145 / 255, blue: 171 / 255, alpha: 1)) // Use `NSExpression` to gradually increase the stroke width between zoom levels 14 and 18. - casingLayer.lineWidth = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", [14: 1, 18: 4]) + casingLayer.lineWidth = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [14: 1, 18: 4])) // Just for fun, let’s add another copy of the line with a dash pattern. let dashedLayer = MLNLineStyleLayer(identifier: "polyline-dash", source: source) diff --git a/platform/ios/MapLibre.docc/MapLibre.md b/platform/ios/MapLibre.docc/MapLibre.md index 2d95991da..a2d603b02 100644 --- a/platform/ios/MapLibre.docc/MapLibre.md +++ b/platform/ios/MapLibre.docc/MapLibre.md @@ -18,6 +18,7 @@ Powerful, free and open-source mapping toolkit with full control over data sourc - - - +- ## Topics diff --git a/platform/ios/MapLibre.docc/WebAPIDataExampleUIKit.md b/platform/ios/MapLibre.docc/WebAPIDataExampleUIKit.md new file mode 100644 index 000000000..629f4d3b4 --- /dev/null +++ b/platform/ios/MapLibre.docc/WebAPIDataExampleUIKit.md @@ -0,0 +1,224 @@ +# Showing data from an API (UIKit) + +Showing data from an API with custom styling and interaction + +This example loads lighthouses in the United States from [WikiData](http://tinyurl.com/zrl2jc4). It adds points to the map and applies dynamic styling to these points. When zooming in the dots become larger circles with a custom icon and the name of the lighthouse shown next to it. When tapping a callout is shown with the name of the lighthouse that was tapped on. + + + +```swift +class WebAPIDataExampleUIKit: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: view.bounds) + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + mapView.setCenter(CLLocationCoordinate2D(latitude: 37.090240, longitude: -95.712891), zoomLevel: 2, animated: false) + mapView.delegate = self + mapView.attributionButton.isHidden = true + view.addSubview(mapView) + + // Add a single tap gesture recognizer. This gesture requires the built-in MLNMapView tap gestures (such as those for zoom and annotation selection) to fail. + let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleMapTap(sender:))) + for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer { + singleTap.require(toFail: recognizer) + } + mapView.addGestureRecognizer(singleTap) + } + + func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) { + fetchPoints { [weak self] features in + self?.addItemsToMap(features: features) + } + } + + func addItemsToMap(features: [MLNPointFeature]) { + // MLNMapView.style is optional, so you must guard against it not being set. + guard let style = mapView.style else { return } + + // You can add custom UIImages to the map style. + // These can be referenced by an MLNSymbolStyleLayer’s iconImage property. + style.setImage(UIImage(named: "lighthouse")!, forName: "lighthouse") + + // Add the features to the map as a shape source. + let source = MLNShapeSource(identifier: "us-lighthouses", features: features, options: nil) + style.addSource(source) + + let lighthouseColor = UIColor(red: 0.08, green: 0.44, blue: 0.96, alpha: 1.0) + + // Use MLNCircleStyleLayer to represent the points with simple circles. + // In this case, we can use style functions to gradually change properties between zoom level 2 and 7: the circle opacity from 50% to 100% and the circle radius from 2pt to 3pt. + + let circles = MLNCircleStyleLayer(identifier: "lighthouse-circles", source: source) + circles.circleColor = NSExpression(forConstantValue: lighthouseColor) + + // The circles should increase in opacity from 0.5 to 1 based on zoom level. + circles.circleOpacity = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [2: 0.5, 7: 1])) + circles.circleRadius = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [2: 2, 7: 3])) + + // Use MLNSymbolStyleLayer for more complex styling of points including custom icons and text rendering. + let symbols = MLNSymbolStyleLayer(identifier: "lighthouse-symbols", source: source) + symbols.iconImageName = NSExpression(forConstantValue: "lighthouse") + symbols.iconColor = NSExpression(forConstantValue: lighthouseColor) + symbols.iconScale = NSExpression(forConstantValue: 0.5) + symbols.iconHaloColor = NSExpression(forConstantValue: UIColor.white.withAlphaComponent(0.5)) + symbols.iconHaloWidth = NSExpression(forConstantValue: 1) + symbols.iconOpacity = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [5.9: 0, 6: 1])) + + // "name" references the "name" key in an MLNPointFeature’s attributes dictionary. + symbols.text = NSExpression(forKeyPath: "name") + symbols.textColor = symbols.iconColor + symbols.textFontSize = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [10: 10, 16: 16])) + symbols.textTranslation = NSExpression(forConstantValue: NSValue(cgVector: CGVector(dx: 15, dy: 0))) + symbols.textOpacity = symbols.iconOpacity + symbols.textHaloColor = symbols.iconHaloColor + symbols.textHaloWidth = symbols.iconHaloWidth + symbols.textJustification = NSExpression(forConstantValue: NSValue(mlnTextJustification: .left)) + symbols.textAnchor = NSExpression(forConstantValue: NSValue(mlnTextAnchor: .left)) + + style.addLayer(circles) + style.addLayer(symbols) + } + + // MARK: - Feature interaction + + @IBAction func handleMapTap(sender: UITapGestureRecognizer) { + if sender.state == .ended { + // Limit feature selection to just the following layer identifiers. + let layerIdentifiers: Set = ["lighthouse-symbols", "lighthouse-circles"] + + // Try matching the exact point first. + let point = sender.location(in: sender.view!) + for feature in mapView.visibleFeatures(at: point, styleLayerIdentifiers: layerIdentifiers) + where feature is MLNPointFeature + { + guard let selectedFeature = feature as? MLNPointFeature else { + fatalError("Failed to cast selected feature as MLNPointFeature") + } + showCallout(feature: selectedFeature) + return + } + + let touchCoordinate = mapView.convert(point, toCoordinateFrom: sender.view!) + let touchLocation = CLLocation(latitude: touchCoordinate.latitude, longitude: touchCoordinate.longitude) + + // Otherwise, get all features within a rect the size of a touch (44x44). + let touchRect = CGRect(origin: point, size: .zero).insetBy(dx: -22.0, dy: -22.0) + let possibleFeatures = mapView.visibleFeatures(in: touchRect, styleLayerIdentifiers: Set(layerIdentifiers)).filter { $0 is MLNPointFeature } + + // Select the closest feature to the touch center. + let closestFeatures = possibleFeatures.sorted(by: { + CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude).distance(from: touchLocation) < CLLocation(latitude: $1.coordinate.latitude, longitude: $1.coordinate.longitude).distance(from: touchLocation) + }) + if let feature = closestFeatures.first { + guard let closestFeature = feature as? MLNPointFeature else { + fatalError("Failed to cast selected feature as MLNPointFeature") + } + showCallout(feature: closestFeature) + return + } + + // If no features were found, deselect the selected annotation, if any. + mapView.deselectAnnotation(mapView.selectedAnnotations.first, animated: true) + } + } + + func showCallout(feature: MLNPointFeature) { + let point = MLNPointFeature() + point.title = feature.attributes["name"] as? String + point.coordinate = feature.coordinate + + // Selecting an feature that doesn’t already exist on the map will add a new annotation view. + // We’ll need to use the map’s delegate methods to add an empty annotation view and remove it when we’re done selecting it. + mapView.selectAnnotation(point, animated: true, completionHandler: nil) + } + + // MARK: - MLNMapViewDelegate + + func mapView(_: MLNMapView, annotationCanShowCallout _: MLNAnnotation) -> Bool { + true + } + + func mapView(_ mapView: MLNMapView, didDeselect annotation: MLNAnnotation) { + mapView.removeAnnotations([annotation]) + } + + func mapView(_: MLNMapView, viewFor _: MLNAnnotation) -> MLNAnnotationView? { + // Create an empty view annotation. Set a frame to offset the callout. + MLNAnnotationView(frame: CGRect(x: 0, y: 0, width: 20, height: 20)) + } + + // MARK: - Data fetching and parsing + + func fetchPoints(withCompletion completion: @escaping (([MLNPointFeature]) -> Void)) { + // Wikidata query for all lighthouses in the United States: http://tinyurl.com/zrl2jc4 + let query = "SELECT DISTINCT ?item " + + "?itemLabel ?coor ?image " + + "WHERE " + + "{ " + + "?item wdt:P31 wd:Q39715 . " + + "?item wdt:P17 wd:Q30 . " + + "?item wdt:P625 ?coor . " + + "OPTIONAL { ?item wdt:P18 ?image } . " + + "SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\" } " + + "} " + + "ORDER BY ?itemLabel" + + let characterSet = NSMutableCharacterSet() + characterSet.formUnion(with: CharacterSet.urlQueryAllowed) + characterSet.removeCharacters(in: "?") + characterSet.removeCharacters(in: "&") + characterSet.removeCharacters(in: ":") + + let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: characterSet as CharacterSet)! + + let request = URLRequest(url: URL(string: "https://query.wikidata.org/sparql?query=\(encodedQuery)&format=json")!) + + URLSession.shared.dataTask(with: request, completionHandler: { data, _, error in + guard error == nil else { + preconditionFailure("Failed to load GeoJSON data: \(error!)") + } + + guard + let data, + let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject], + let results = json["results"] as? [String: AnyObject], + let items = results["bindings"] as? [[String: AnyObject]] + else { + preconditionFailure("Failed to parse GeoJSON data") + } + + DispatchQueue.main.async { + completion(self.parseJSONItems(items: items)) + } + }).resume() + } + + func parseJSONItems(items: [[String: AnyObject]]) -> [MLNPointFeature] { + var features = [MLNPointFeature]() + for item in items { + guard let label = item["itemLabel"] as? [String: AnyObject], + let title = label["value"] as? String else { continue } + guard let coor = item["coor"] as? [String: AnyObject], + let point = coor["value"] as? String else { continue } + + let parsedPoint = point.replacingOccurrences(of: "Point(", with: "").replacingOccurrences(of: ")", with: "") + let pointComponents = parsedPoint.components(separatedBy: " ") + let coordinate = CLLocationCoordinate2D(latitude: Double(pointComponents[1])!, longitude: Double(pointComponents[0])!) + let feature = MLNPointFeature() + feature.coordinate = coordinate + feature.title = title + // A feature’s attributes can used by runtime styling for things like text labels. + feature.attributes = [ + "name": title, + ] + features.append(feature) + } + return features + } +} +``` + +![](WebAPIDataExample.png) \ No newline at end of file diff --git a/platform/ios/app-swift/BUILD.bazel b/platform/ios/app-swift/BUILD.bazel index bcbff495c..67b6fbdfd 100644 --- a/platform/ios/app-swift/BUILD.bazel +++ b/platform/ios/app-swift/BUILD.bazel @@ -10,6 +10,9 @@ configure_device_profiles() swift_library( name = "Sources", srcs = glob(["Sources/*.swift"]), + data = glob([ + "Data/*", + ]), module_name = "Sources", tags = ["manual"], deps = [ @@ -27,9 +30,6 @@ ios_application( infoplists = [":Info.plist"], minimum_os_version = "16.0", provisioning_profile = "xcode_profile", - resources = glob([ - "Data/*", - ]), visibility = ["@rules_xcodeproj//xcodeproj:generated"], deps = [":Sources"], ) diff --git a/platform/ios/app-swift/Data/lighthouse.png b/platform/ios/app-swift/Data/lighthouse.png new file mode 100644 index 000000000..1c253e759 Binary files /dev/null and b/platform/ios/app-swift/Data/lighthouse.png differ diff --git a/platform/ios/app-swift/Sources/LineStyleLayerExampleUIKit.swift b/platform/ios/app-swift/Sources/LineStyleLayerExampleUIKit.swift index fd8de6076..58aa8c08a 100644 --- a/platform/ios/app-swift/Sources/LineStyleLayerExampleUIKit.swift +++ b/platform/ios/app-swift/Sources/LineStyleLayerExampleUIKit.swift @@ -69,8 +69,7 @@ class LineStyleLayerExampleUIKit: UIViewController, MLNMapViewDelegate { layer.lineColor = NSExpression(forConstantValue: UIColor(red: 59 / 255, green: 178 / 255, blue: 208 / 255, alpha: 1)) // Use `NSExpression` to smoothly adjust the line width from 2pt to 20pt between zoom levels 14 and 18. The `interpolationBase` parameter allows the values to interpolate along an exponential curve. - layer.lineWidth = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", - [14: 2, 18: 20]) + layer.lineWidth = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [14: 2, 18: 20])) // We can also add a second layer that will draw a stroke around the original line. let casingLayer = MLNLineStyleLayer(identifier: "polyline-case", source: source) @@ -82,7 +81,7 @@ class LineStyleLayerExampleUIKit: UIViewController, MLNMapViewDelegate { // Stroke color slightly darker than the line color. casingLayer.lineColor = NSExpression(forConstantValue: UIColor(red: 41 / 255, green: 145 / 255, blue: 171 / 255, alpha: 1)) // Use `NSExpression` to gradually increase the stroke width between zoom levels 14 and 18. - casingLayer.lineWidth = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", [14: 1, 18: 4]) + casingLayer.lineWidth = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [14: 1, 18: 4])) // Just for fun, let’s add another copy of the line with a dash pattern. let dashedLayer = MLNLineStyleLayer(identifier: "polyline-dash", source: source) diff --git a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift index efca188fb..e6ea2545d 100644 --- a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift +++ b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift @@ -20,6 +20,9 @@ struct MapLibreNavigationView: View { NavigationLink("LineStyleLayerExample") { LineStyleLayerExampleUIViewControllerRepresentable() } + NavigationLink("WebAPIDataExampleUIKit") { + WebAPIDataExampleUIViewControllerRepresentable() + } } } } diff --git a/platform/ios/app-swift/Sources/WebAPIDataExampleUIKit.swift b/platform/ios/app-swift/Sources/WebAPIDataExampleUIKit.swift new file mode 100644 index 000000000..1fe07d1e7 --- /dev/null +++ b/platform/ios/app-swift/Sources/WebAPIDataExampleUIKit.swift @@ -0,0 +1,230 @@ + +import MapLibre +import SwiftUI +import UIKit + +// #-example-code(WebAPIDataExampleUIKit) +class WebAPIDataExampleUIKit: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: view.bounds) + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + mapView.setCenter(CLLocationCoordinate2D(latitude: 37.090240, longitude: -95.712891), zoomLevel: 2, animated: false) + mapView.delegate = self + mapView.attributionButton.isHidden = true + view.addSubview(mapView) + + // Add a single tap gesture recognizer. This gesture requires the built-in MLNMapView tap gestures (such as those for zoom and annotation selection) to fail. + let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleMapTap(sender:))) + for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer { + singleTap.require(toFail: recognizer) + } + mapView.addGestureRecognizer(singleTap) + } + + func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) { + fetchPoints { [weak self] features in + self?.addItemsToMap(features: features) + } + } + + func addItemsToMap(features: [MLNPointFeature]) { + // MLNMapView.style is optional, so you must guard against it not being set. + guard let style = mapView.style else { return } + + // You can add custom UIImages to the map style. + // These can be referenced by an MLNSymbolStyleLayer’s iconImage property. + style.setImage(UIImage(named: "lighthouse")!, forName: "lighthouse") + + // Add the features to the map as a shape source. + let source = MLNShapeSource(identifier: "us-lighthouses", features: features, options: nil) + style.addSource(source) + + let lighthouseColor = UIColor(red: 0.08, green: 0.44, blue: 0.96, alpha: 1.0) + + // Use MLNCircleStyleLayer to represent the points with simple circles. + // In this case, we can use style functions to gradually change properties between zoom level 2 and 7: the circle opacity from 50% to 100% and the circle radius from 2pt to 3pt. + + let circles = MLNCircleStyleLayer(identifier: "lighthouse-circles", source: source) + circles.circleColor = NSExpression(forConstantValue: lighthouseColor) + + // The circles should increase in opacity from 0.5 to 1 based on zoom level. + circles.circleOpacity = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [2: 0.5, 7: 1])) + circles.circleRadius = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [2: 2, 7: 3])) + + // Use MLNSymbolStyleLayer for more complex styling of points including custom icons and text rendering. + let symbols = MLNSymbolStyleLayer(identifier: "lighthouse-symbols", source: source) + symbols.iconImageName = NSExpression(forConstantValue: "lighthouse") + symbols.iconColor = NSExpression(forConstantValue: lighthouseColor) + symbols.iconScale = NSExpression(forConstantValue: 0.5) + symbols.iconHaloColor = NSExpression(forConstantValue: UIColor.white.withAlphaComponent(0.5)) + symbols.iconHaloWidth = NSExpression(forConstantValue: 1) + symbols.iconOpacity = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [5.9: 0, 6: 1])) + + // "name" references the "name" key in an MLNPointFeature’s attributes dictionary. + symbols.text = NSExpression(forKeyPath: "name") + symbols.textColor = symbols.iconColor + symbols.textFontSize = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [10: 10, 16: 16])) + symbols.textTranslation = NSExpression(forConstantValue: NSValue(cgVector: CGVector(dx: 15, dy: 0))) + symbols.textOpacity = symbols.iconOpacity + symbols.textHaloColor = symbols.iconHaloColor + symbols.textHaloWidth = symbols.iconHaloWidth + symbols.textJustification = NSExpression(forConstantValue: NSValue(mlnTextJustification: .left)) + symbols.textAnchor = NSExpression(forConstantValue: NSValue(mlnTextAnchor: .left)) + + style.addLayer(circles) + style.addLayer(symbols) + } + + // MARK: - Feature interaction + + @IBAction func handleMapTap(sender: UITapGestureRecognizer) { + if sender.state == .ended { + // Limit feature selection to just the following layer identifiers. + let layerIdentifiers: Set = ["lighthouse-symbols", "lighthouse-circles"] + + // Try matching the exact point first. + let point = sender.location(in: sender.view!) + for feature in mapView.visibleFeatures(at: point, styleLayerIdentifiers: layerIdentifiers) + where feature is MLNPointFeature + { + guard let selectedFeature = feature as? MLNPointFeature else { + fatalError("Failed to cast selected feature as MLNPointFeature") + } + showCallout(feature: selectedFeature) + return + } + + let touchCoordinate = mapView.convert(point, toCoordinateFrom: sender.view!) + let touchLocation = CLLocation(latitude: touchCoordinate.latitude, longitude: touchCoordinate.longitude) + + // Otherwise, get all features within a rect the size of a touch (44x44). + let touchRect = CGRect(origin: point, size: .zero).insetBy(dx: -22.0, dy: -22.0) + let possibleFeatures = mapView.visibleFeatures(in: touchRect, styleLayerIdentifiers: Set(layerIdentifiers)).filter { $0 is MLNPointFeature } + + // Select the closest feature to the touch center. + let closestFeatures = possibleFeatures.sorted(by: { + CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude).distance(from: touchLocation) < CLLocation(latitude: $1.coordinate.latitude, longitude: $1.coordinate.longitude).distance(from: touchLocation) + }) + if let feature = closestFeatures.first { + guard let closestFeature = feature as? MLNPointFeature else { + fatalError("Failed to cast selected feature as MLNPointFeature") + } + showCallout(feature: closestFeature) + return + } + + // If no features were found, deselect the selected annotation, if any. + mapView.deselectAnnotation(mapView.selectedAnnotations.first, animated: true) + } + } + + func showCallout(feature: MLNPointFeature) { + let point = MLNPointFeature() + point.title = feature.attributes["name"] as? String + point.coordinate = feature.coordinate + + // Selecting an feature that doesn’t already exist on the map will add a new annotation view. + // We’ll need to use the map’s delegate methods to add an empty annotation view and remove it when we’re done selecting it. + mapView.selectAnnotation(point, animated: true, completionHandler: nil) + } + + // MARK: - MLNMapViewDelegate + + func mapView(_: MLNMapView, annotationCanShowCallout _: MLNAnnotation) -> Bool { + true + } + + func mapView(_ mapView: MLNMapView, didDeselect annotation: MLNAnnotation) { + mapView.removeAnnotations([annotation]) + } + + func mapView(_: MLNMapView, viewFor _: MLNAnnotation) -> MLNAnnotationView? { + // Create an empty view annotation. Set a frame to offset the callout. + MLNAnnotationView(frame: CGRect(x: 0, y: 0, width: 20, height: 20)) + } + + // MARK: - Data fetching and parsing + + func fetchPoints(withCompletion completion: @escaping (([MLNPointFeature]) -> Void)) { + // Wikidata query for all lighthouses in the United States: http://tinyurl.com/zrl2jc4 + let query = "SELECT DISTINCT ?item " + + "?itemLabel ?coor ?image " + + "WHERE " + + "{ " + + "?item wdt:P31 wd:Q39715 . " + + "?item wdt:P17 wd:Q30 . " + + "?item wdt:P625 ?coor . " + + "OPTIONAL { ?item wdt:P18 ?image } . " + + "SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\" } " + + "} " + + "ORDER BY ?itemLabel" + + let characterSet = NSMutableCharacterSet() + characterSet.formUnion(with: CharacterSet.urlQueryAllowed) + characterSet.removeCharacters(in: "?") + characterSet.removeCharacters(in: "&") + characterSet.removeCharacters(in: ":") + + let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: characterSet as CharacterSet)! + + let request = URLRequest(url: URL(string: "https://query.wikidata.org/sparql?query=\(encodedQuery)&format=json")!) + + URLSession.shared.dataTask(with: request, completionHandler: { data, _, error in + guard error == nil else { + preconditionFailure("Failed to load GeoJSON data: \(error!)") + } + + guard + let data, + let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject], + let results = json["results"] as? [String: AnyObject], + let items = results["bindings"] as? [[String: AnyObject]] + else { + preconditionFailure("Failed to parse GeoJSON data") + } + + DispatchQueue.main.async { + completion(self.parseJSONItems(items: items)) + } + }).resume() + } + + func parseJSONItems(items: [[String: AnyObject]]) -> [MLNPointFeature] { + var features = [MLNPointFeature]() + for item in items { + guard let label = item["itemLabel"] as? [String: AnyObject], + let title = label["value"] as? String else { continue } + guard let coor = item["coor"] as? [String: AnyObject], + let point = coor["value"] as? String else { continue } + + let parsedPoint = point.replacingOccurrences(of: "Point(", with: "").replacingOccurrences(of: ")", with: "") + let pointComponents = parsedPoint.components(separatedBy: " ") + let coordinate = CLLocationCoordinate2D(latitude: Double(pointComponents[1])!, longitude: Double(pointComponents[0])!) + let feature = MLNPointFeature() + feature.coordinate = coordinate + feature.title = title + // A feature’s attributes can used by runtime styling for things like text labels. + feature.attributes = [ + "name": title, + ] + features.append(feature) + } + return features + } +} + +// #-end-example-code + +struct WebAPIDataExampleUIViewControllerRepresentable: UIViewControllerRepresentable { + typealias UIViewControllerType = WebAPIDataExampleUIKit + + func makeUIViewController(context _: Context) -> WebAPIDataExampleUIKit { + WebAPIDataExampleUIKit() + } + + func updateUIViewController(_: WebAPIDataExampleUIKit, context _: Context) {} +}