diff --git a/MODULE.bazel b/MODULE.bazel index 2b24366ac..728b691bc 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -22,3 +22,17 @@ use_repo(pnpm, "pnpm") provisioning_profile_repository = use_extension("@build_bazel_rules_apple//apple:apple.bzl", "provisioning_profile_repository_extension") provisioning_profile_repository.setup() + +http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") + +# This file was created as follows: +# Download shapefile from NPS +# https://public-nps.opendata.arcgis.com/datasets/a40e2faa953b4c5cb7fe10004dc3008e_0/explore?location=0.213550%2C-12.488250%2C2.00 +# ogr2ogr -f GeoJSON pois.json -t_srs EPSG:4326 nps-pois.shp +# tippecanoe -o pois-nps.mbtiles pois.json +http_file( + name = "pois_nps_mbtiles", + downloaded_file_path = "pois-nps.mbtiles", + sha256 = "6a218702a5a60bb6358494217cac2836a087355ac092f6b01f089d8b1b2842ca", + url = "https://maplibre-native.s3.eu-central-1.amazonaws.com/ios-swift-example-app-resources/pois-nps.mbtiles", +) diff --git a/platform/ios/MapLibre.docc/MultipleImagesExample.md b/platform/ios/MapLibre.docc/MultipleImagesExample.md new file mode 100644 index 000000000..584fab205 --- /dev/null +++ b/platform/ios/MapLibre.docc/MultipleImagesExample.md @@ -0,0 +1,99 @@ +# Adding Multiple Images + +Adding images to the map and assigning them to POI types + +> This example uses UIKit. + +This example uses POIs in national parks and adds them to the map with data and icons provided by the National Park Service. Both the data and the icons are in the public domain. + +## Preparation + +We've done all the work for you and generated [`pois-nps.mbtiles`](https://maplibre-native.s3.eu-central-1.amazonaws.com/ios-swift-example-app-resources/pois-nps.mbtiles), which you can download directly. +You can skip the rest of this section unless you're curious how we built the MBTiles from the source data. + +- Data, Shapefile download: [https://public-nps.opendata.arcgis.com/datasets/a40e2faa953b4c5cb7fe10004dc3008e_0/](https://public-nps.opendata.arcgis.com/datasets/a40e2faa953b4c5cb7fe10004dc3008e_0/) +- Icons: [https://www.nps.gov/subjects/gisandmapping/map-symbols-patterns-for-nps-maps.htm](https://www.nps.gov/subjects/gisandmapping/map-symbols-patterns-for-nps-maps.htm) + +Convert the data first to GeoJSON format with [`ogr2ogr`](https://gdal.org/programs/ogr2ogr.html) and then to GeoJSON format using [tippecanoe](https://github.com/felt/tippecanoe). + +``` +ogr2ogr -f GeoJSON pois.json -t_srs EPSG:4326 nps-pois.shp +tippecanoe -o pois-nps.mbtiles pois.json +``` + +The resulting `.mbtiles` file can be hosted with a tile server such as [Martin](https://martin.maplibre.org/) or embedded in the app bundle as a resource. Since the file is quite small in this case we will use that last option in this example. Martin comes with a [`mbtiles` binary](https://maplibre.org/martin/mbtiles-meta.html) that allows us to inspect what the MBTiles file contains from the command line. + +``` +mbtiles meta-all pois-nps.mbtiles +``` + +The important thing to note is that there is a `pois` layer whose features include a `POITYPE` attribute. While we could add icons for all kinds of `POITYPE` values included in the dataset, we will only add icons for restrooms, trailheads and viewpoints, and leave the rest as an exercise to the reader. + +The icons need to be extracted with a tool like [Inkscape](https://inkscape.org/) because the National Park Service includes all icons in a big vector file. This is outside the scope of this example. + +## Adding Icons on Style Load + +The `.mbtiles` file needs to be added to the assets of the app. When the style loads we can add a ``MLNVectorTileSource`` with as URL `mbtiles://\(Bundle.main.bundlePath)/pois-nps.mbtiles"`. + +The images need to be added to an imageset so they can be loaded as an `UIImage` and added to the style with ``MLNStyle/setImage:forName:`` as is shown below in the example. Note that you should set ``MLNVectorStyleLayer/sourceLayerIdentifier`` to match the layer name in the MBTiles file. Lastly a [`match` expression](https://maplibre.org/maplibre-style-spec/expressions/#match) is used to select the correct image based on `POITYPE` attribute present in the feature. + + + +```swift +class MultipleImagesExample: 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 + + let glacierPoint = CLLocationCoordinate2D(latitude: 37.72836462778902, longitude: -119.57352616838511) + mapView.setCenter(glacierPoint, animated: false) + mapView.zoomLevel = 12 + + 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: "pois-nps", configurationURL: URL(string: "mbtiles://\(Bundle.main.bundlePath)/pois-nps.mbtiles")!) + + style.addSource(source) + + let imagesToAdd = [ + ["nps-restrooms", "restrooms"], + ["nps-trailhead", "trailhead"], + ["nps-viewpoint", "viewpoint"], + ] + + for imageInfo in imagesToAdd { + if let imageName = imageInfo.first, let imageKey = imageInfo.last { + if let image = UIImage(named: imageName) { + style.setImage(image, forName: imageKey) + } else { + print("Failed to load \(imageName)") + return + } + } + } + + let imageLayer = MLNSymbolStyleLayer(identifier: "npc-poi-images", source: source) + imageLayer.sourceLayerIdentifier = "pois" + imageLayer.iconImageName = NSExpression(mglJSONObject: [ + "match", ["get", "POITYPE"], + "Restroom", "restrooms", + "Trailhead", "trailhead", + "Viewpoint", "viewpoint", + "", + ]) + + style.addLayer(imageLayer) + } +} +``` + +![](MultipleImagesExample.png) \ No newline at end of file diff --git a/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/16px-nps-restrooms.png b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/16px-nps-restrooms.png new file mode 100644 index 000000000..2b297a7fe Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/16px-nps-restrooms.png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/48px-nps-restrooms .png b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/48px-nps-restrooms .png new file mode 100644 index 000000000..ea99a16a5 Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/48px-nps-restrooms .png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/Contents.json b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/Contents.json new file mode 100644 index 000000000..b7f7730fc --- /dev/null +++ b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "16px-nps-restrooms.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "nps-restrooms.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "48px-nps-restrooms .png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/nps-restrooms.png b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/nps-restrooms.png new file mode 100644 index 000000000..cba577d51 Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-restrooms.imageset/nps-restrooms.png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/16px-nps-trailhead.png b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/16px-nps-trailhead.png new file mode 100644 index 000000000..33ba54307 Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/16px-nps-trailhead.png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/48x-nps-trailhead.png b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/48x-nps-trailhead.png new file mode 100644 index 000000000..1d97178be Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/48x-nps-trailhead.png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/Contents.json b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/Contents.json new file mode 100644 index 000000000..48c8b9bf8 --- /dev/null +++ b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "16px-nps-trailhead.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "nps-trailhead.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "48x-nps-trailhead.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/nps-trailhead.png b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/nps-trailhead.png new file mode 100644 index 000000000..7914f7033 Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-trailhead.imageset/nps-trailhead.png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Contents.json b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Contents.json new file mode 100644 index 000000000..f3b8a8ddd --- /dev/null +++ b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Viewpoint-16.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Viewpoint-32.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Viewpoint-48.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-16.png b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-16.png new file mode 100644 index 000000000..a627fb21e Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-16.png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-32.png b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-32.png new file mode 100644 index 000000000..8c252c7bd Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-32.png differ diff --git a/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-48.png b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-48.png new file mode 100644 index 000000000..d60baa7f8 Binary files /dev/null and b/platform/ios/app-swift/Assets.xcassets/nps-viewpoint.imageset/Viewpoint-48.png differ diff --git a/platform/ios/app-swift/BUILD.bazel b/platform/ios/app-swift/BUILD.bazel index dcd0a60af..495a52b2c 100644 --- a/platform/ios/app-swift/BUILD.bazel +++ b/platform/ios/app-swift/BUILD.bazel @@ -31,6 +31,9 @@ ios_application( infoplists = [":Info.plist"], minimum_os_version = "16.0", provisioning_profile = "xcode_profile", + resources = [ + "@pois_nps_mbtiles//file", + ], visibility = ["@rules_xcodeproj//xcodeproj:generated"], deps = [":Sources"], ) diff --git a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift index fbfccd4e9..9b98c14e0 100644 --- a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift +++ b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift @@ -45,6 +45,9 @@ struct MapLibreNavigationView: View { NavigationLink("POIAlongRouteExample") { POIAlongRouteExampleUIViewControllerRepresentable() } + NavigationLink("MultipleImagesExample") { + MultipleImagesExampleUIViewControllerRepresentable().edgesIgnoringSafeArea(.all) + } } } } diff --git a/platform/ios/app-swift/Sources/MultipleImagesExample.swift b/platform/ios/app-swift/Sources/MultipleImagesExample.swift new file mode 100644 index 000000000..bc077e367 --- /dev/null +++ b/platform/ios/app-swift/Sources/MultipleImagesExample.swift @@ -0,0 +1,72 @@ + +import MapLibre +import SwiftUI +import UIKit + +// #-example-code(MultipleImagesExample) +class MultipleImagesExample: 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 + + let glacierPoint = CLLocationCoordinate2D(latitude: 37.72836462778902, longitude: -119.57352616838511) + mapView.setCenter(glacierPoint, animated: false) + mapView.zoomLevel = 12 + + 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: "pois-nps", configurationURL: URL(string: "mbtiles://\(Bundle.main.bundlePath)/pois-nps.mbtiles")!) + + style.addSource(source) + + let imagesToAdd = [ + ["nps-restrooms", "restrooms"], + ["nps-trailhead", "trailhead"], + ["nps-viewpoint", "viewpoint"], + ] + + for imageInfo in imagesToAdd { + if let imageName = imageInfo.first, let imageKey = imageInfo.last { + if let image = UIImage(named: imageName) { + style.setImage(image, forName: imageKey) + } else { + print("Failed to load \(imageName)") + return + } + } + } + + let imageLayer = MLNSymbolStyleLayer(identifier: "npc-poi-images", source: source) + imageLayer.sourceLayerIdentifier = "pois" + imageLayer.iconImageName = NSExpression(mglJSONObject: [ + "match", ["get", "POITYPE"], + "Restroom", "restrooms", + "Trailhead", "trailhead", + "Viewpoint", "viewpoint", + "", + ]) + + style.addLayer(imageLayer) + } +} + +// #-end-example-code + +struct MultipleImagesExampleUIViewControllerRepresentable: UIViewControllerRepresentable { + typealias UIViewControllerType = MultipleImagesExample + + func makeUIViewController(context _: Context) -> MultipleImagesExample { + MultipleImagesExample() + } + + func updateUIViewController(_: MultipleImagesExample, context _: Context) {} +}