From 6a8adaa5c3f680ec35e4d5e54a28328df8072d44 Mon Sep 17 00:00:00 2001 From: Bart Louwers Date: Mon, 8 Jul 2024 16:57:06 +0200 Subject: [PATCH] iOS Documentation: Snapshotter (#2543) --- platform/darwin/src/MLNMapSnapshotter.h | 6 + platform/darwin/src/MLNMapSnapshotter.mm | 9 - platform/darwin/src/MLNSource.h | 2 +- platform/darwin/src/MLNVectorTileSource.h | 2 +- platform/ios/CHANGELOG.md | 2 +- .../MapLibre.docc/DDSCircleLayerExample.md | 55 ++++++ .../MapLibre.docc/LineStyleLayerExample.md | 4 +- platform/ios/MapLibre.docc/MapLibre.md | 27 ++- .../ios/MapLibre.docc/POIAlongRouteExample.md | 159 +++++++++++++++++ .../MapLibre.docc/StaticSnapshotExample.md | 66 +++++++ .../ios/MapLibre.docc/WebAPIDataExample.md | 4 +- .../Sources/DDSCircleLayerExample.swift | 55 ++++++ .../Sources/MapLibreNavigationView.swift | 27 ++- .../Sources/POIAlongRouteExample.swift | 161 ++++++++++++++++++ .../Sources/StaticSnapShotExample.swift | 71 ++++++++ 15 files changed, 619 insertions(+), 31 deletions(-) create mode 100644 platform/ios/MapLibre.docc/DDSCircleLayerExample.md create mode 100644 platform/ios/MapLibre.docc/POIAlongRouteExample.md create mode 100644 platform/ios/MapLibre.docc/StaticSnapshotExample.md create mode 100644 platform/ios/app-swift/Sources/DDSCircleLayerExample.swift create mode 100644 platform/ios/app-swift/Sources/POIAlongRouteExample.swift create mode 100644 platform/ios/app-swift/Sources/StaticSnapShotExample.swift diff --git a/platform/darwin/src/MLNMapSnapshotter.h b/platform/darwin/src/MLNMapSnapshotter.h index 29ea1bb57..b2ba073c4 100644 --- a/platform/darwin/src/MLNMapSnapshotter.h +++ b/platform/darwin/src/MLNMapSnapshotter.h @@ -77,6 +77,12 @@ MLN_EXPORT // MARK: - Configuring the Map +/** + :nodoc: + Whether to include the MapLibre logo. Note this is not required. + */ +@property (nonatomic, readwrite) BOOL showsLogo; + /** URL of the map style to snapshot. */ diff --git a/platform/darwin/src/MLNMapSnapshotter.mm b/platform/darwin/src/MLNMapSnapshotter.mm index ff85544f2..cc926cfda 100644 --- a/platform/darwin/src/MLNMapSnapshotter.mm +++ b/platform/darwin/src/MLNMapSnapshotter.mm @@ -130,15 +130,6 @@ private: #endif @end -@interface MLNMapSnapshotOptions () - -/** - :nodoc: - Whether to include the MapLibre logo. Note this is not required. - */ -@property (nonatomic, readwrite) BOOL showsLogo; -@end - @implementation MLNMapSnapshotOptions - (instancetype _Nonnull)initWithStyleURL:(nullable NSURL *)styleURL camera:(MLNMapCamera *)camera size:(CGSize)size diff --git a/platform/darwin/src/MLNSource.h b/platform/darwin/src/MLNSource.h index e2acbefa5..1117289e5 100644 --- a/platform/darwin/src/MLNSource.h +++ b/platform/darwin/src/MLNSource.h @@ -21,7 +21,7 @@ FOUNDATION_EXTERN MLN_EXPORT MLNExceptionName const MLNInvalidStyleSourceExcepti Create instances of `MLNShapeSource`, `MLNComputedShapeSource`, `MLNImageSource`, and the concrete subclasses of `MLNTileSource` - (`MLNVectorTileSource` and `MLNRasterTileSource`) in order to use `MLNSource`’s + (``MLNVectorTileSource`` and `MLNRasterTileSource`) in order to use `MLNSource`’s properties and methods. Do not create instances of `MLNSource` directly, and do not create your own subclasses of this class. */ diff --git a/platform/darwin/src/MLNVectorTileSource.h b/platform/darwin/src/MLNVectorTileSource.h index d845b15ad..87a0b2b8c 100644 --- a/platform/darwin/src/MLNVectorTileSource.h +++ b/platform/darwin/src/MLNVectorTileSource.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN class may be a suitable alternative. Each - vector + vector source defined by the style JSON file is represented at runtime by an `MLNVectorTileSource` object that you can use to initialize new style layers. You can also add and remove sources dynamically using methods such as diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 16a7a8acb..bdfa81ffb 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -5,7 +5,7 @@ MapLibre welcomes participation and contributions from everyone. Please read [`C ## main - Add [Privacy Manifest](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files). MapLibre Native iOS has no built-in tracking, but it does use some system APIs for functional purposes that are marked by Apple as privacy sensitive. ([#1866](https://github.com/maplibre/maplibre-native/issues/1866)). -- Change default `MLNMapSnapshotter` logo to the MapLibre logo ([#2541](https://github.com/maplibre/maplibre-native/pull/2541)). Note that showing the MapLibre logo is never required. Check with your tile provider if you need to show a logo. +- Change default `MLNMapSnapshotter` logo to the MapLibre logo ([#2541](https://github.com/maplibre/maplibre-native/pull/2541)). Note that showing the MapLibre logo is never required. You can configure whether to show the logo with the (now public) `showsLogo` property of `MLNMapSnapshotterOptions`. Check with your tile provider if you need to show a logo. ## 6.5.0 diff --git a/platform/ios/MapLibre.docc/DDSCircleLayerExample.md b/platform/ios/MapLibre.docc/DDSCircleLayerExample.md new file mode 100644 index 000000000..9f1326427 --- /dev/null +++ b/platform/ios/MapLibre.docc/DDSCircleLayerExample.md @@ -0,0 +1,55 @@ +# Vector Tile Sources + +Add and style a vector tile source + +> This example uses UIKit + +This example shows how a vector data source can be added and a style for it can be configured dynamically. + +The tiles are [tiles around Innsbruck, Austria](https://github.com/maplibre/demotiles/tree/gh-pages/tiles-omt) that use the OpenMapTiles schema. We are interested in the [POIs](https://openmaptiles.org/schema/#poi) that are in the `poi` layer, and filter this further with an `NSPredicate` to only show POIs with a `class` of shop. Each POI has a `rank` which is normally used to reduce label density. In this example, we use it to demonstrate how a numeric attribute can be used for styling with the [step](https://maplibre.org/maplibre-style-spec/expressions/#step) expression. POIs with a rank between 0 and 10 get a red color, between 10 and 20 green, etc. + + + +```swift +class DDSCircleLayerExample: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: view.bounds, styleURL: AMERICANA_STYLE) + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + mapView.tintColor = .darkGray + + // Set the map’s center coordinate and zoom level. + let innsbruck = CLLocationCoordinate2D(latitude: 47.26497, longitude: 11.4088) + mapView.setCenter(innsbruck, animated: false) + mapView.zoomLevel = 14 + + mapView.delegate = self + view.addSubview(mapView) + } + + // Wait until the style is loaded before modifying the map style. + func mapView(_: MLNMapView, didFinishLoading style: MLNStyle) { + let source = MLNVectorTileSource(identifier: "demotiles", configurationURL: URL(string: "https://demotiles.maplibre.org/tiles-omt/tiles.json")!) + + style.addSource(source) + + let layer = MLNCircleStyleLayer(identifier: "poi-shop-style", source: source) + + layer.sourceLayerIdentifier = "poi" + layer.predicate = NSPredicate(format: "class == %@", "shop") + + // Style the circle layer color based on the rank + layer.circleColor = NSExpression(mglJSONObject: ["step", ["get", "rank"], 0, "red", 10, "green", 20, "blue", 30, "purple", 40, "yellow"] as [Any]) + layer.circleRadius = NSExpression(forConstantValue: 3) + + style.addLayer(layer) + } +} +``` + +![](DDSCircleLayerExample.png) + +Map data from OpenStreetMap. Style uses OpenMapTiles. \ No newline at end of file diff --git a/platform/ios/MapLibre.docc/LineStyleLayerExample.md b/platform/ios/MapLibre.docc/LineStyleLayerExample.md index eb9bea017..4f18e2e81 100644 --- a/platform/ios/MapLibre.docc/LineStyleLayerExample.md +++ b/platform/ios/MapLibre.docc/LineStyleLayerExample.md @@ -1,9 +1,9 @@ # Using GeoJSON with a line style layer -> Note: This example uses UIKit. - Adding an ``MLNLineStyleLayer`` to the map using a GeoJSON file. +> Note: This example uses UIKit. + ```swift diff --git a/platform/ios/MapLibre.docc/MapLibre.md b/platform/ios/MapLibre.docc/MapLibre.md index 943e251ec..538fe214a 100644 --- a/platform/ios/MapLibre.docc/MapLibre.md +++ b/platform/ios/MapLibre.docc/MapLibre.md @@ -15,19 +15,32 @@ Powerful, free and open-source mapping toolkit with full control over data sourc [MapLibre Native](https://github.com/maplibre/maplibre-native) is a map rendering toolkit with support for iOS. It can be used as an alternative to MapKit. You have full control over the data sources used for rendering the map, as well as the styling. You can even participate in the development as MapLibre Native is free and open-source project. > Note: For information on creating and modifying map styles, see the [MapLibre Style Spec documentation](https://maplibre.org/maplibre-style-spec/). -## Guides +## Topics + +### Essentials - -- -- -- -- -- +- + +### Styling and Dynamic Data + - +- +- +- +- + +### Map Interaction + +- +- - - -## Topics +### Features + +- +- ### Map diff --git a/platform/ios/MapLibre.docc/POIAlongRouteExample.md b/platform/ios/MapLibre.docc/POIAlongRouteExample.md new file mode 100644 index 000000000..55cbf19e5 --- /dev/null +++ b/platform/ios/MapLibre.docc/POIAlongRouteExample.md @@ -0,0 +1,159 @@ +# POI Along a Route + +Use an `NSPredicate` to show POI and road labels along a route. + +> This example uses UIKit. + +This example adds a dynamically styled GeoJSON route to the map, similar to . However, two existing layers: the `poi` layer and the `road_label` part of the Americana style are adjusted as well. The contents of these layers are shown or hidden, based on whether they lay inside a polygon around the route. In this example, both the route and the area of the polygon along the route are hardcoded. + +The route is styled with three `MLNLineStyleLayer`s. We make use of the [interpolate expression](https://maplibre.org/maplibre-style-spec/expressions/#interpolate) to set the widths of these line layers at various zoom levels. + + + + +```swift +class POIAlongRouteExample: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: view.bounds, styleURL: AMERICANA_STYLE) + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + mapView.setCenter( + CLLocationCoordinate2D(latitude: 45.52214, longitude: -122.63748), + zoomLevel: 18, + animated: false + ) + view.addSubview(mapView) + mapView.delegate = self + } + + // Wait until the map is loaded before adding to the map. + func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) { + loadGeoJson() + restrictPOIVisibleShape() + setCamera() + } + + func loadGeoJson() { + DispatchQueue.global().async { + // Get the path for example.geojson in the app’s bundle. + guard let jsonUrl = Bundle.main.url(forResource: "example", withExtension: "geojson") else { + preconditionFailure("Failed to load local GeoJSON file") + } + + guard let jsonData = try? Data(contentsOf: jsonUrl) else { + preconditionFailure("Failed to parse GeoJSON file") + } + + DispatchQueue.main.async { + self.drawPolyline(geoJson: jsonData) + } + } + } + + func setCamera() { + let camera = mapView.camera + camera.heading = 249.37706203842038 + camera.pitch = 60 + camera.centerCoordinate.latitude = 45.52199780570582 + camera.centerCoordinate.longitude = -122.6418837958432 + mapView.setCamera(camera, animated: false) + mapView.setZoomLevel(15.062187320447523, animated: false) + } + + func drawPolyline(geoJson: Data) { + // Add our GeoJSON data to the map as an MLNGeoJSONSource. + // We can then reference this data from an MLNStyleLayer. + + // MLNMapView.style is optional, so you must guard against it not being set. + guard let style = mapView.style else { return } + + guard let shapeFromGeoJSON = try? MLNShape(data: geoJson, encoding: String.Encoding.utf8.rawValue) else { + fatalError("Could not generate MLNShape") + } + + let source = MLNShapeSource(identifier: "polyline", shape: shapeFromGeoJSON, options: nil) + style.addSource(source) + + // Create new layer for the line. + let layer = MLNLineStyleLayer(identifier: "polyline", source: source) + + // Set the line join and cap to a rounded end. + layer.lineJoin = NSExpression(forConstantValue: "round") + layer.lineCap = NSExpression(forConstantValue: "round") + + // Set the line color to a constant blue color. + layer.lineColor = NSExpression(forConstantValue: UIColor(red: 59 / 255, green: 178 / 255, blue: 208 / 255, alpha: 1)) + + // Use expression to smoothly adjust the line width from 2pt to 20pt between zoom levels 14 and 18. + layer.lineWidth = NSExpression(mglJSONObject: ["interpolate", ["linear"], ["zoom"], 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) + // Copy these attributes from the main line layer. + casingLayer.lineJoin = layer.lineJoin + casingLayer.lineCap = layer.lineCap + // Line gap width represents the space before the outline begins, so should match the main line’s line width exactly. + casingLayer.lineGapWidth = layer.lineWidth + // 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 expression to gradually increase the stroke width between zoom levels 14 and 18. + casingLayer.lineWidth = NSExpression(mglJSONObject: ["interpolate", ["linear"], ["zoom"], 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) + dashedLayer.lineJoin = layer.lineJoin + dashedLayer.lineCap = layer.lineCap + dashedLayer.lineColor = NSExpression(forConstantValue: UIColor.white) + dashedLayer.lineOpacity = NSExpression(forConstantValue: 0.5) + dashedLayer.lineWidth = layer.lineWidth + // Dash pattern in the format [dash, gap, dash, gap, ...]. You’ll want to adjust these values based on the line cap style. + dashedLayer.lineDashPattern = NSExpression(forConstantValue: [0, 1.5]) + + guard let poiLayer = mapView.style?.layer(withIdentifier: "poi") as? MLNSymbolStyleLayer else { + print("Could not find poi layer") + return + } + style.insertLayer(layer, below: poiLayer) + style.insertLayer(dashedLayer, above: layer) + style.insertLayer(casingLayer, below: layer) + } + + func restrictPOIVisibleShape() { + // find poi-label layer + guard let poiLayer = mapView.style?.layer(withIdentifier: "poi") as? MLNSymbolStyleLayer else { + print("Could not find poi layer") + return + } + // find road-label layer + guard let roadLabelLayer = mapView.style?.layer(withIdentifier: "road_label") as? MLNSymbolStyleLayer else { + print("Could not find road_label layer") + return + } + // show the POI and road that is within this polygon + let polygonShape = [ + [-122.63730626171188, 45.52288837762333], + [-122.65455070022612, 45.52299746891552], + [-122.65747018755947, 45.52177017968134], + [-122.65992255691913, 45.51931552089448], + [-122.66015611590598, 45.513696676587045], + [-122.66696825301655, 45.51375123117057], + [-122.6672018120034, 45.51222368283956], + [-122.6571977020749, 45.51225096085216], + [-122.6570419960839, 45.51822452705878], + [-122.65392787626189, 45.52106106703124], + [-122.63567134880579, 45.52114288817623], + [-122.63657745074761, 45.52288036393409], + [-122.6373404839605, 45.52291377640398], + ] + // create a polygon class + let coordinates = polygonShape.map { CLLocationCoordinate2D(latitude: $0[1], longitude: $0[0]) } + let bufferedRoutePolygon = MLNPolygon(coordinates: coordinates, count: UInt(coordinates.count), interiorPolygons: nil) + // apply predicates to these two layers + poiLayer.predicate = NSPredicate(format: "SELF IN %@", bufferedRoutePolygon) + roadLabelLayer.predicate = NSPredicate(format: "SELF IN %@", bufferedRoutePolygon) + } +} +``` \ No newline at end of file diff --git a/platform/ios/MapLibre.docc/StaticSnapshotExample.md b/platform/ios/MapLibre.docc/StaticSnapshotExample.md new file mode 100644 index 000000000..5646c495e --- /dev/null +++ b/platform/ios/MapLibre.docc/StaticSnapshotExample.md @@ -0,0 +1,66 @@ +# Making Snapshots + +Use ``MLNMapSnapshotter`` to create snapshots of the map. + +> This example uses UIKit. + +This example demonstrates how snapshots can be made of the map. The result is an `UIImage` that you can let the user save to their camera roll or share with a friend. In this demo, the resulting image is shown back with an `UIImageView`. Attribution is read from the sources and added to the snapshot (when applicable). By default a MapLibre logo is included on the image, but it is not required to show it. You can use ``MLNMapSnapshotOptions/showsLogo`` to configure whether to include the logo. Check with your tile provider if you need to show their logo. + + + +```swift +class StaticSnapshotExample: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + var button: UIButton! + var imageView: UIImageView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height / 2), styleURL: AMERICANA_STYLE) + mapView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + + // Center map on the Giza Pyramid Complex in Egypt. + let center = CLLocationCoordinate2D(latitude: 29.9773, longitude: 31.1325) + mapView.setCenter(center, zoomLevel: 14, animated: false) + view.addSubview(mapView) + + // Create a button to take a map snapshot. + button = UIButton(frame: CGRect(x: mapView.bounds.width / 2 - 40, y: mapView.bounds.height - 40, width: 80, height: 30)) + button.layer.cornerRadius = 15 + button.backgroundColor = UIColor(red: 0.96, green: 0.65, blue: 0.14, alpha: 1.0) + button.setImage(UIImage(named: "camera"), for: .normal) + button.addTarget(self, action: #selector(createSnapshot), for: .touchUpInside) + view.addSubview(button) + + // Create a UIImageView that will store the map snapshot. + imageView = UIImageView(frame: CGRect(x: 0, y: view.bounds.height / 2, width: view.bounds.width, height: view.bounds.height / 2)) + imageView.backgroundColor = .black + imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(imageView) + } + + @objc func createSnapshot() { + // Use the map's style, camera, size, and zoom level to set the snapshot's options. + let options = MLNMapSnapshotOptions(styleURL: mapView.styleURL, camera: mapView.camera, size: mapView.bounds.size) + options.zoomLevel = mapView.zoomLevel + + // Add an activity indicator to show that the snapshot is loading. + let indicator = UIActivityIndicatorView(frame: CGRect(x: imageView.center.x - 30, y: imageView.center.y - 30, width: 60, height: 60)) + view.addSubview(indicator) + indicator.startAnimating() + + // Create the map snapshot. + let snapshotter: MLNMapSnapshotter? = MLNMapSnapshotter(options: options) + snapshotter?.start { snapshot, error in + if error != nil { + print("Unable to create a map snapshot.") + } else if let snapshot { + // Add the map snapshot's image to the image view. + indicator.stopAnimating() + self.imageView.image = snapshot.image + } + } + } +} +``` \ No newline at end of file diff --git a/platform/ios/MapLibre.docc/WebAPIDataExample.md b/platform/ios/MapLibre.docc/WebAPIDataExample.md index 20f0ef33c..87e062248 100644 --- a/platform/ios/MapLibre.docc/WebAPIDataExample.md +++ b/platform/ios/MapLibre.docc/WebAPIDataExample.md @@ -1,9 +1,9 @@ # Showing data from an API -> Note: This example uses UIKit. - Showing data from an API with custom styling and interaction +> Note: This example uses UIKit. + 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. diff --git a/platform/ios/app-swift/Sources/DDSCircleLayerExample.swift b/platform/ios/app-swift/Sources/DDSCircleLayerExample.swift new file mode 100644 index 000000000..da75dd59f --- /dev/null +++ b/platform/ios/app-swift/Sources/DDSCircleLayerExample.swift @@ -0,0 +1,55 @@ + +import MapLibre +import SwiftUI +import UIKit + +// #-example-code(DDSCircleLayerExample) +class DDSCircleLayerExample: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: view.bounds, styleURL: AMERICANA_STYLE) + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + mapView.tintColor = .darkGray + + // Set the map’s center coordinate and zoom level. + let innsbruck = CLLocationCoordinate2D(latitude: 47.26497, longitude: 11.4088) + mapView.setCenter(innsbruck, animated: false) + mapView.zoomLevel = 14 + + mapView.delegate = self + view.addSubview(mapView) + } + + // Wait until the style is loaded before modifying the map style. + func mapView(_: MLNMapView, didFinishLoading style: MLNStyle) { + let source = MLNVectorTileSource(identifier: "demotiles", configurationURL: URL(string: "https://demotiles.maplibre.org/tiles-omt/tiles.json")!) + + style.addSource(source) + + let layer = MLNCircleStyleLayer(identifier: "poi-shop-style", source: source) + + layer.sourceLayerIdentifier = "poi" + layer.predicate = NSPredicate(format: "class == %@", "shop") + + // Style the circle layer color based on the rank + layer.circleColor = NSExpression(mglJSONObject: ["step", ["get", "rank"], 0, "red", 10, "green", 20, "blue", 30, "purple", 40, "yellow"] as [Any]) + layer.circleRadius = NSExpression(forConstantValue: 3) + + style.addLayer(layer) + } +} + +// #-end-example-code + +struct DDSCircleLayerExampleUIViewControllerRepresentable: UIViewControllerRepresentable { + typealias UIViewControllerType = DDSCircleLayerExample + + func makeUIViewController(context _: Context) -> DDSCircleLayerExample { + DDSCircleLayerExample() + } + + func updateUIViewController(_: DDSCircleLayerExample, context _: Context) {} +} diff --git a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift index ae12528f5..fbfccd4e9 100644 --- a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift +++ b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift @@ -26,14 +26,25 @@ struct MapLibreNavigationView: View { NavigationLink("AddMarkerExample") { AddMarkerSymbolExampleUIViewControllerRepresentable() } - NavigationLink("AnimatedLineExample") { - AnimatedLineExampleUIViewControllerRepresentable() - } - NavigationLink("AnnotationViewExample") { - AnnotationViewExampleUIViewControllerRepresentable() - } - NavigationLink("BuildingLightExample") { - BuildingLightExampleUIViewControllerRepresentable() + Group { + NavigationLink("AnimatedLineExample") { + AnimatedLineExampleUIViewControllerRepresentable() + } + NavigationLink("AnnotationViewExample") { + AnnotationViewExampleUIViewControllerRepresentable() + } + NavigationLink("BuildingLightExample") { + BuildingLightExampleUIViewControllerRepresentable() + } + NavigationLink("StaticSnapshotExample") { + StaticSnapshotExampleUIViewControllerRepresentable() + } + NavigationLink("DDSCircleLayerExample") { + DDSCircleLayerExampleUIViewControllerRepresentable().edgesIgnoringSafeArea(.all) + } + NavigationLink("POIAlongRouteExample") { + POIAlongRouteExampleUIViewControllerRepresentable() + } } } } diff --git a/platform/ios/app-swift/Sources/POIAlongRouteExample.swift b/platform/ios/app-swift/Sources/POIAlongRouteExample.swift new file mode 100644 index 000000000..7959828f6 --- /dev/null +++ b/platform/ios/app-swift/Sources/POIAlongRouteExample.swift @@ -0,0 +1,161 @@ +import MapLibre +import SwiftUI +import UIKit + +// #-example-code(POIAlongRouteExample) +class POIAlongRouteExample: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: view.bounds, styleURL: AMERICANA_STYLE) + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + mapView.setCenter( + CLLocationCoordinate2D(latitude: 45.52214, longitude: -122.63748), + zoomLevel: 18, + animated: false + ) + view.addSubview(mapView) + mapView.delegate = self + } + + // Wait until the map is loaded before adding to the map. + func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) { + loadGeoJson() + restrictPOIVisibleShape() + setCamera() + } + + func loadGeoJson() { + DispatchQueue.global().async { + // Get the path for example.geojson in the app’s bundle. + guard let jsonUrl = Bundle.main.url(forResource: "example", withExtension: "geojson") else { + preconditionFailure("Failed to load local GeoJSON file") + } + + guard let jsonData = try? Data(contentsOf: jsonUrl) else { + preconditionFailure("Failed to parse GeoJSON file") + } + + DispatchQueue.main.async { + self.drawPolyline(geoJson: jsonData) + } + } + } + + func setCamera() { + let camera = mapView.camera + camera.heading = 249.37706203842038 + camera.pitch = 60 + camera.centerCoordinate.latitude = 45.52199780570582 + camera.centerCoordinate.longitude = -122.6418837958432 + mapView.setCamera(camera, animated: false) + mapView.setZoomLevel(15.062187320447523, animated: false) + } + + func drawPolyline(geoJson: Data) { + // Add our GeoJSON data to the map as an MLNGeoJSONSource. + // We can then reference this data from an MLNStyleLayer. + + // MLNMapView.style is optional, so you must guard against it not being set. + guard let style = mapView.style else { return } + + guard let shapeFromGeoJSON = try? MLNShape(data: geoJson, encoding: String.Encoding.utf8.rawValue) else { + fatalError("Could not generate MLNShape") + } + + let source = MLNShapeSource(identifier: "polyline", shape: shapeFromGeoJSON, options: nil) + style.addSource(source) + + // Create new layer for the line. + let layer = MLNLineStyleLayer(identifier: "polyline", source: source) + + // Set the line join and cap to a rounded end. + layer.lineJoin = NSExpression(forConstantValue: "round") + layer.lineCap = NSExpression(forConstantValue: "round") + + // Set the line color to a constant blue color. + layer.lineColor = NSExpression(forConstantValue: UIColor(red: 59 / 255, green: 178 / 255, blue: 208 / 255, alpha: 1)) + + // Use expression to smoothly adjust the line width from 2pt to 20pt between zoom levels 14 and 18. + layer.lineWidth = NSExpression(mglJSONObject: ["interpolate", ["linear"], ["zoom"], 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) + // Copy these attributes from the main line layer. + casingLayer.lineJoin = layer.lineJoin + casingLayer.lineCap = layer.lineCap + // Line gap width represents the space before the outline begins, so should match the main line’s line width exactly. + casingLayer.lineGapWidth = layer.lineWidth + // 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 expression to gradually increase the stroke width between zoom levels 14 and 18. + casingLayer.lineWidth = NSExpression(mglJSONObject: ["interpolate", ["linear"], ["zoom"], 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) + dashedLayer.lineJoin = layer.lineJoin + dashedLayer.lineCap = layer.lineCap + dashedLayer.lineColor = NSExpression(forConstantValue: UIColor.white) + dashedLayer.lineOpacity = NSExpression(forConstantValue: 0.5) + dashedLayer.lineWidth = layer.lineWidth + // Dash pattern in the format [dash, gap, dash, gap, ...]. You’ll want to adjust these values based on the line cap style. + dashedLayer.lineDashPattern = NSExpression(forConstantValue: [0, 1.5]) + + guard let poiLayer = mapView.style?.layer(withIdentifier: "poi") as? MLNSymbolStyleLayer else { + print("Could not find poi layer") + return + } + style.insertLayer(layer, below: poiLayer) + style.insertLayer(dashedLayer, above: layer) + style.insertLayer(casingLayer, below: layer) + } + + func restrictPOIVisibleShape() { + // find poi-label layer + guard let poiLayer = mapView.style?.layer(withIdentifier: "poi") as? MLNSymbolStyleLayer else { + print("Could not find poi layer") + return + } + // find road-label layer + guard let roadLabelLayer = mapView.style?.layer(withIdentifier: "road_label") as? MLNSymbolStyleLayer else { + print("Could not find road_label layer") + return + } + // show the POI and road that is within this polygon + let polygonShape = [ + [-122.63730626171188, 45.52288837762333], + [-122.65455070022612, 45.52299746891552], + [-122.65747018755947, 45.52177017968134], + [-122.65992255691913, 45.51931552089448], + [-122.66015611590598, 45.513696676587045], + [-122.66696825301655, 45.51375123117057], + [-122.6672018120034, 45.51222368283956], + [-122.6571977020749, 45.51225096085216], + [-122.6570419960839, 45.51822452705878], + [-122.65392787626189, 45.52106106703124], + [-122.63567134880579, 45.52114288817623], + [-122.63657745074761, 45.52288036393409], + [-122.6373404839605, 45.52291377640398], + ] + // create a polygon class + let coordinates = polygonShape.map { CLLocationCoordinate2D(latitude: $0[1], longitude: $0[0]) } + let bufferedRoutePolygon = MLNPolygon(coordinates: coordinates, count: UInt(coordinates.count), interiorPolygons: nil) + // apply predicates to these two layers + poiLayer.predicate = NSPredicate(format: "SELF IN %@", bufferedRoutePolygon) + roadLabelLayer.predicate = NSPredicate(format: "SELF IN %@", bufferedRoutePolygon) + } +} + +// #-end-example-code + +struct POIAlongRouteExampleUIViewControllerRepresentable: UIViewControllerRepresentable { + typealias UIViewControllerType = POIAlongRouteExample + + func makeUIViewController(context _: Context) -> POIAlongRouteExample { + POIAlongRouteExample() + } + + func updateUIViewController(_: POIAlongRouteExample, context _: Context) {} +} diff --git a/platform/ios/app-swift/Sources/StaticSnapShotExample.swift b/platform/ios/app-swift/Sources/StaticSnapShotExample.swift new file mode 100644 index 000000000..faaaac320 --- /dev/null +++ b/platform/ios/app-swift/Sources/StaticSnapShotExample.swift @@ -0,0 +1,71 @@ +import MapLibre +import SwiftUI +import UIKit + +// #-example-code(StaticSnapshotExample) +class StaticSnapshotExample: UIViewController, MLNMapViewDelegate { + var mapView: MLNMapView! + var button: UIButton! + var imageView: UIImageView! + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = MLNMapView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height / 2), styleURL: AMERICANA_STYLE) + mapView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + + // Center map on the Giza Pyramid Complex in Egypt. + let center = CLLocationCoordinate2D(latitude: 29.9773, longitude: 31.1325) + mapView.setCenter(center, zoomLevel: 14, animated: false) + view.addSubview(mapView) + + // Create a button to take a map snapshot. + button = UIButton(frame: CGRect(x: mapView.bounds.width / 2 - 40, y: mapView.bounds.height - 40, width: 80, height: 30)) + button.layer.cornerRadius = 15 + button.backgroundColor = UIColor(red: 0.96, green: 0.65, blue: 0.14, alpha: 1.0) + button.setImage(UIImage(named: "camera"), for: .normal) + button.addTarget(self, action: #selector(createSnapshot), for: .touchUpInside) + view.addSubview(button) + + // Create a UIImageView that will store the map snapshot. + imageView = UIImageView(frame: CGRect(x: 0, y: view.bounds.height / 2, width: view.bounds.width, height: view.bounds.height / 2)) + imageView.backgroundColor = .black + imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(imageView) + } + + @objc func createSnapshot() { + // Use the map's style, camera, size, and zoom level to set the snapshot's options. + let options = MLNMapSnapshotOptions(styleURL: mapView.styleURL, camera: mapView.camera, size: mapView.bounds.size) + options.zoomLevel = mapView.zoomLevel + + // Add an activity indicator to show that the snapshot is loading. + let indicator = UIActivityIndicatorView(frame: CGRect(x: imageView.center.x - 30, y: imageView.center.y - 30, width: 60, height: 60)) + view.addSubview(indicator) + indicator.startAnimating() + + // Create the map snapshot. + let snapshotter: MLNMapSnapshotter? = MLNMapSnapshotter(options: options) + snapshotter?.start { snapshot, error in + if error != nil { + print("Unable to create a map snapshot.") + } else if let snapshot { + // Add the map snapshot's image to the image view. + indicator.stopAnimating() + self.imageView.image = snapshot.image + } + } + } +} + +// #-end-example-code + +struct StaticSnapshotExampleUIViewControllerRepresentable: UIViewControllerRepresentable { + typealias UIViewControllerType = StaticSnapshotExample + + func makeUIViewController(context _: Context) -> StaticSnapshotExample { + StaticSnapshotExample() + } + + func updateUIViewController(_: StaticSnapshotExample, context _: Context) {} +}