v4ulthunt3r/swiftoverpassapi
A Swift module for querying, decoding, and visualizing Overpass API data.
**Installation**
SwiftOverpassAPI is available through Swift Package Manager. To install it, add the following to your Package.swift file:
dependencies: [
.package(url: "https://github.com/V4ulthunt3r/SwiftOverpassAPI.git", from: "1.0.0")
]Or add it directly in Xcode:
- Go to File > Add Packages...
- Enter the repository URL: https://github.com/v4ulthunt3r/SwiftOverpassAPI.git
- Click Add Package
**Usage**
### **Creating a bounding box**
Create a boxed region that will confine your query:
**Option 1:** Initialize with a MKCoordinateRegion:
```swift
let center = CLLocationCoordinate2D(
latitude: 49.450103,
longitude: 11.075683)
let queryRegion = MKCoordinateRegion(
center: center,
latitudinalMeters: 50000,
longitudinalMeters: 50000)
let boundingBox = OPBoundingBox(region: region)
```
**Option 2:** Initialize with latitudes and longitudes:
```swift
let boundingBox = OPBoundingBox(
minLatitude: 38.62661651293796,
minLongitude: -90.1998908782745,
maxLatitude: 38.627383487062005,
maxLongitude: -90.1989091217254)
```
### **Building a Query**
For simple query generation, you can use `OPQueryBuilder` class:
```swift
do {
let query = try OPQueryBuilder()
.setTimeOut(180) //1
.setElementTypes([.relation]) //2
.addTagFilter(key: "amenity", value: "biergarten") //3
.addTagFilter(key: "name") //4
.setBoundingBox(boundingBox) //5
.setOutputType(.geometry) //6
.buildQueryString() //7
} catch {
print(error.localizedDescription)
}
```
1) Set a timeout for the server request
2) Set one or more element types that you wish to query (Any combination of `.node`, `.way` and/or `.relation`)
3) Filter for elements whose "amenity" tag's value is exactly "biergarten"
4) Filter for all elements with a "name" tag. Can have any associated value.
5) Query within the specified bounding box
6) Specify the output type of the query (See "Choosing a query output type" below)
7) Build a query string that you pass to the overpass client that makes requests to an Overpass API endpoint
The Overpass Query language enables diverse and powerful queries. This makes building a catch-all query builder quite difficult. For more complicated queries, you may need to create the query string directly:
```swift
let query = """
data=[out:json];
area["name"="Nürnberg"]->.nuremberg;
(
node(area.nuremberg)["railway"="station"]["station"="subway"];
)->.subway_stations;
(
node(around.subway_stations:400)["amenity"="cinema"];
way(around.subway_stations:400)["amenity"="cinema"];
relation(around.subway_stations:400)["amenity"="cinema"];
);
out center;
"""
```
This query finds all theaters less than 400 meters from any subway stop in nuremberg. To learn more about the Overpass Query Language, I recommend checking out out the [Overpass Language Guide](https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide#Recursing_up_and_down:_Completed_ways_and_relations), the [Overpass Query Language Wiki](https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL), and [Overpass API by Example](https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_API_by_Example). You can test overpass queries in your browser using [Overpass Turbo](https://overpass-turbo.eu/).
### **Choosing a query output type**
When using `OPQueryBuiler` you can choose from the following output types:
```swift
public enum OPQueryOutputType {
case standard, center, geometry, recurseDown, recurseUp, recurseUpAndDown
// The Overpass API language syntax for each output type
func toString() -> String {
switch self {
case .standard:
return "out;"
case .recurseDown:
return "(._;>;);out;"
case .recurseUp:
return "(._;<;);out;"
case .recurseUpAndDown:
return "((._;<;);>;);out;"
case .geometry:
return "out geom;"
case .center:
return "out center;"
}
}
}
```
- **Standard:** Basic output that does not fetch additional elements or geometry information
- **Recurse Down:** Enables full geometry reconstruction of query elements. Returns the queried elements plus:
- all nodes that are part of a way which appears in the initial result set; plus
- all nodes and ways that are members of a relation which appears in the initial result set; plus
- all nodes that are part of a way which appears in the initial result set
- **Recurse Up:** Returns the queried elements plus:
- all ways that have a node which appears in the initial result set
- all relations that have a node or way which appears in the initial result set
- all relations that have a way which appears in the result initial result set
- **Recurse Up and Down:** Recurse up then recurse down on the results of the upwards recursion
- **Geometry:** Returned elements full geometry information that is sufficient for visualization
- **Center:** Returned elements contain their center coordinate. Best/most efficient option when you don't want to visualize full element geometries.
### **Making an Overpass request**
```swift
let client = OPClient() //1
client.endpoint = .kumiSystems //2
//3
do {
let elements = try await client.fetchElements(query: query)
print(elements) // Do something with the returned elements
} catch {
print(error.localizedDescription)
}
```
1) Instantiate a client
2) Specify an endpoint: The free-to-use endpoints provided will typically be slower and may limit your usage. For better performance you can specify your own custom endpoint.
3) Fetch elements: The decoded response will be in the form of a dictionary of Overpass elements keyed by their database id. The function is now async and uses modern Swift concurrency with try/catch error handling.
### **Generating MapKit Visualizations**
Generate visualizations for all elements the returned element dictionary:
```swift
// Creates a dictionary of mapkit visualizations keyed by the corresponding element's id
let visualizations = OPVisualizationGenerator
.mapKitVisualizations(forElements: elements)
```
Generate a visualization for an individual element:
```swift
if let visualization = OPVisualizationGenerator.mapKitVisualization(forElement: element) {
// Do something
} else {
print("Element doesn't have a geometry to visualize")
}
```
### **Displaying Visualizations via MKMapView**
**Step 1:** Add overlays and annotations to mapView using the included visualization generator
```swift
func addVisualizations(_ visualizations: [Int: OPMapKitVisualization]) {
self.visualizations = visualizations
removeAnnotations?(annotations)
removeOverlays?(overlays)
annotations = []
overlays = []
var newAnnotations = [MKAnnotation]()
var polylines = [MKPolyline]()
var polygons = [MKPolygon]()
for visualization in visualizations.values {
switch visualization {
case .annotation(let annotation):
newAnnotations.append(annotation)
case .polyline(let polyline):
polylines.append(polyline)
case .polylines(let newPolylines):
polylines.append(contentsOf: newPolylines)
case .polygon(let polygon):
polygons.append(polygon)
case .polygons(let newPolygons):
polygons.append(contentsOf: newPolygons)
}
}
let multiPolyline = MKMultiPolyline(polylines)
let multiPolygon = MKMultiPolygon(polygons)
let newOverlays: [MKOverlay] = [multiPolyline, multiPolygon]
annotations = newAnnotations
overlays = newOverlays
addAnnotations?(annotations)
addOverlays?(overlays)
}
```
Depending on its case, a visualization can have one of the following associated values types:
1) `MKAnnotation`: For single coordinates. The title of the annotation is the value of the element's name tag.
2) `MKPolyline`: Commonly used for roads
3) `MKPolygon`: Commonly used for simple structures like buildings
4) `[MKPolyline]`: An array of related polylines in a collection such as a route or a waterway
5) `[MKPolygon]`: An array of related polygons that make up a more complicated structures.
**Step 2:** Display views for the overlays and annotations
```swift
extension MapViewController: MKMapViewDelegate {
// Delegate method for rendering overlays
func mapView(
_ mapView: MKMapView,
rendererFor overlay: MKOverlay) -> MKOverlayRenderer
{
let strokeWidth: CGFloat = 2
let strokeColor = UIColor.theme
let fillColor = UIColor.theme.withAlphaComponent(0.5)
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(
polyline: polyline)
renderer.strokeColor = strokeColor
renderer.lineWidth = strokeWidth
return renderer
} else if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(
polygon: polygon)
renderer.fillColor = fillColor
renderer.strokeColor = strokeColor
renderer.lineWidth = strokeWidth
return renderer
} else if let multiPolyline = overlay as? MKMultiPolyline {
let renderer = MKMultiPolylineRenderer(
multiPolyline: multiPolyline)
renderer.strokeColor = strokeColor
renderer.lineWidth = strokeWidth
return renderer
} else if let multiPolygon = overlay as? MKMultiPolygon {
let renderer = MKMultiPolygonRenderer(
multiPolygon: multiPolygon)
renderer.fillColor = fillColor
renderer.strokeColor = strokeColor
renderer.lineWidth = strokeWidth
return renderer
} else {
return MKOverlayRenderer()
}
}
/*
// Make sure to add the following when configure your mapView:
let markerReuseIdentifier = "MarkerAnnotationView"
mapView.register(
MKMarkerAnnotationView.self,
forAnnotationViewWithReuseIdentifier: markerReuseIdentifier)
*/
// Delegate method for setting annotation views.
func mapView(
_ mapView: MKMapView,
viewFor annotation: MKAnnotation) -> MKAnnotationView?
{
guard
let pointAnnotation = annotation as? MKPointAnnotation
else {
return nil
}
let view = MKMarkerAnnotationView(
annotation: pointAnnotation,
reuseIdentifier: markerReuseIdentifier)
view.markerTintColor = UIColor.theme
return view
}
}
```
### **Displaying Visualizations via SwiftUI Map**
To display Overpass API data in a SwiftUI Map view, you can use the following approach:
```swift
import SwiftUI
import MapKit
import SwiftOverpassAPI
struct MapView: View {
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)
@State private var elements: [Int: OPElement] = [:]
var body: some View {
Map(coordinateRegion: $region, annotationItems: annotations) { annotation in
MapAnnotation(coordinate: annotation.coordinate) {
Text(annotation.title ?? "")
.padding(5)
.background(Color.white)
.cornerRadius(5)
}
}
.overlay(overlays)
.task {
await fetchData()
}
}
private var annotations: [MKPointAnnotation] {
elements.values.compactMap { element in
guard let visualization = OPVisualizationGenerator.mapKitVisualization(forElement: element),
case .annotation(let annotation) = visualization else {
return nil
}
return annotation
}
}
private var overlays: some View {
GeometryReader { geometry in
ForEach(elements.values.compactMap { element -> (Int, OPMapKitVisualization)? in
guard let visualization = OPVisualizationGenerator.mapKitVisualization(forElement: element) else {
return nil
}
return (element.id, visualization)
}, id: \.0) { _, visualization in
switch visualization {
case .polyline(let polyline):
MapPolyline(polyline: polyline)
.stroke(Color.blue, lineWidth: 2)
case .polygon(let polygon):
MapPolygon(polygon: polygon)
.fill(Color.blue.opacity(0.3))
.stroke(Color.blue, lineWidth: 2)
case .polylines(let polylines):
ForEach(polylines, id: \.self) { polyline in
MapPolyline(polyline: polyline)
.stroke(Color.blue, lineWidth: 2)
}
case .polygons(let polygons):
ForEach(polygons, id: \.self) { polygon in
MapPolygon(polygon: polygon)
.fill(Color.blue.opacity(0.3))
.stroke(Color.blue, lineWidth: 2)
}
case .annotation:
EmptyView()
}
}
}
}
private func fetchData() async {
let boundingBox = OPBoundingBox(region: region)
let query = try? OPQueryBuilder()
.setElementTypes([.node, .way, .relation])
.setBoundingBox(boundingBox)
.setOutputType(.geometry)
.buildQueryString()
guard let query = query else { return }
let client = OPClient()
client.endpoint = .kumiSystems
do {
elements = try await client.fetchElements(query: query)
} catch {
print(error.localizedDescription)
}
}
}
// Helper views for rendering MapKit overlays in SwiftUI
struct MapPolyline: Shape {
let polyline: MKPolyline
func path(in rect: CGRect) -> Path {
var path = Path()
let points = polyline.points()
guard let firstPoint = points.first else { return path }
path.move(to: firstPoint)
for point in points.dropFirst() {
path.addLine(to: point)
}
return path
}
}
struct MapPolygon: Shape {
let polygon: MKPolygon
func path(in rect: CGRect) -> Path {
var path = Path()
let points = polygon.points()
guard let firstPoint = points.first else { return path }
path.move(to: firstPoint)
for point in points.dropFirst() {
path.addLine(to: point)
}
path.closeSubpath()
return path
}
}**Example App**
<p align="center"> <img src="Screenshots/tourist_attractions.png?raw=true" alt="Nuremberg Tourism" width="250"> <img src="Screenshots/subway.png?raw=true" alt="Nuremberg Subway Lines" width="250"> <img src="Screenshots/subway_with_stops.png?raw=true" alt="Nuremberg Subway Lines with Stops" width="250"> <img src="Screenshots/max_morlock.png?raw=true" alt="Bart Nuremberg Max-Morlock-Stadion" width="250"> </p>
To run the example project, clone the repo, and open the project in the SwiftOverpassAPIDemo directory.
**Aknowledgements**
Thanks to all those who contribute to Overpass API and OpenStreetMap. Thank you to Edward Samson, whose osmtogeojson code made this even possible.
**License**
SwiftOverpassAPI is available under the MIT license. See the LICENSE file for more info.
Package Metadata
Repository: v4ulthunt3r/swiftoverpassapi
Default branch: main
README: README.md