├── .gitignore
├── .travis.yml
├── Example
├── Podfile
├── Podfile.lock
├── Pods
│ ├── Local Podspecs
│ │ └── SwiftOverpassAPI.podspec.json
│ ├── Manifest.lock
│ ├── Pods.xcodeproj
│ │ └── project.pbxproj
│ └── Target Support Files
│ │ ├── Pods-SwiftOverpassAPI_Example
│ │ ├── Pods-SwiftOverpassAPI_Example-Info.plist
│ │ ├── Pods-SwiftOverpassAPI_Example-acknowledgements.markdown
│ │ ├── Pods-SwiftOverpassAPI_Example-acknowledgements.plist
│ │ ├── Pods-SwiftOverpassAPI_Example-dummy.m
│ │ ├── Pods-SwiftOverpassAPI_Example-frameworks.sh
│ │ ├── Pods-SwiftOverpassAPI_Example-umbrella.h
│ │ ├── Pods-SwiftOverpassAPI_Example.debug.xcconfig
│ │ ├── Pods-SwiftOverpassAPI_Example.modulemap
│ │ └── Pods-SwiftOverpassAPI_Example.release.xcconfig
│ │ ├── Pods-SwiftOverpassAPI_Tests
│ │ ├── Pods-SwiftOverpassAPI_Tests-Info.plist
│ │ ├── Pods-SwiftOverpassAPI_Tests-acknowledgements.markdown
│ │ ├── Pods-SwiftOverpassAPI_Tests-acknowledgements.plist
│ │ ├── Pods-SwiftOverpassAPI_Tests-dummy.m
│ │ ├── Pods-SwiftOverpassAPI_Tests-umbrella.h
│ │ ├── Pods-SwiftOverpassAPI_Tests.debug.xcconfig
│ │ ├── Pods-SwiftOverpassAPI_Tests.modulemap
│ │ └── Pods-SwiftOverpassAPI_Tests.release.xcconfig
│ │ └── SwiftOverpassAPI
│ │ ├── SwiftOverpassAPI-Info.plist
│ │ ├── SwiftOverpassAPI-dummy.m
│ │ ├── SwiftOverpassAPI-prefix.pch
│ │ ├── SwiftOverpassAPI-umbrella.h
│ │ ├── SwiftOverpassAPI.modulemap
│ │ └── SwiftOverpassAPI.xcconfig
├── SwiftOverpassAPI.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── SwiftOverpassAPI-Example.xcscheme
├── SwiftOverpassAPI.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── SwiftOverpassAPI
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ ├── LaunchScreen.xib
│ │ └── Main.storyboard
│ ├── Controllers
│ │ ├── DemoMapViewModel.swift
│ │ ├── DemoTableViewModel.swift
│ │ ├── DemoViewController.swift
│ │ ├── DemoViewModel.swift
│ │ ├── MapViewController.swift
│ │ ├── PullUpContainer.swift
│ │ ├── SelectDemoTableViewModel.swift
│ │ └── TableViewController.swift
│ ├── Images.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── Models
│ │ └── Demo.swift
│ ├── Navigation
│ │ └── OverpassDemoCoordinator.swift
│ ├── Protocols
│ │ ├── CellRepresentable.swift
│ │ ├── CellSelectable.swift
│ │ ├── MapViewModel.swift
│ │ └── TableViewModel.swift
│ ├── Utilities
│ │ ├── MKCoordinateRegion+Extensions.swift
│ │ ├── NSLayoutContstraint+Extensions.swift
│ │ ├── UIColor+Extensions.swift
│ │ ├── UIView+Extensions.swift
│ │ └── UIViewController+Extensions.swift
│ └── Views
│ │ ├── DemoSelectCellViewModel.swift
│ │ ├── PullUpHeaderView.swift
│ │ └── SelectableCellViewModel.swift
└── Tests
│ ├── Info.plist
│ └── Tests.swift
├── LICENSE
├── README.md
├── Screenshots
├── bart_lines_screenshot.png
├── buildings_screenshot.png
├── busch_stadium_screenshot.png
└── tourism_screenshot.png
├── Source
├── Controllers
│ ├── OPQueryBuilder.swift
│ ├── OPVisualizationGenerator.swift
│ ├── PolygonChecker.swift
│ └── TagChecker.swift
├── Models
│ ├── ElementCodingKeys.swift
│ ├── ElementsCodingKeys.swift
│ ├── NestedPolygonCoordinates.swift
│ ├── OPBoundingBox.swift
│ ├── OPClientResult.swift
│ ├── OPElementCenter.swift
│ ├── OPElementType.swift
│ ├── OPGeometry.swift
│ ├── OPMapKitVisualization.swift
│ ├── OPMeta.swift
│ ├── OPNode.swift
│ ├── OPQueryOutputType.swift
│ ├── OPRelation.swift
│ └── OPWay.swift
├── Networking
│ ├── OPClient.swift
│ └── OPDecodingOperation.swift
├── Protocols
│ └── OPElement.swift
└── Utilities
│ ├── CLLocationCoordinate2D+Extensions.swift
│ ├── Constants.swift
│ ├── DecoderExtractor.swift
│ ├── Errors.swift
│ └── LinkedList.swift
├── SwiftOverpassAPI.podspec
├── SwiftOverpassAPI
├── Assets
│ └── .gitkeep
└── Classes
│ └── .gitkeep
└── _Pods.xcodeproj
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | # Bundler
23 | .bundle
24 |
25 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
26 | # Carthage/Checkouts
27 |
28 | Carthage/Build
29 |
30 | # We recommend against adding the Pods directory to your .gitignore. However
31 | # you should judge for yourself, the pros and cons are mentioned at:
32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
33 | #
34 | # Note: if you ignore the Pods directory, make sure to uncomment
35 | # `pod install` in .travis.yml
36 | #
37 | # Pods/
38 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # references:
2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/
3 | # * https://github.com/supermarin/xcpretty#usage
4 |
5 | osx_image: xcode7.3
6 | language: objective-c
7 | # cache: cocoapods
8 | # podfile: Example/Podfile
9 | # before_install:
10 | # - gem install cocoapods # Since Travis is not always on latest version
11 | # - pod install --project-directory=Example
12 | script:
13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SwiftOverpassAPI.xcworkspace -scheme SwiftOverpassAPI-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty
14 | - pod lib lint
15 |
--------------------------------------------------------------------------------
/Example/Podfile:
--------------------------------------------------------------------------------
1 | use_frameworks!
2 |
3 | target 'SwiftOverpassAPI_Example' do
4 | pod 'SwiftOverpassAPI', :path => '../'
5 |
6 | target 'SwiftOverpassAPI_Tests' do
7 | inherit! :search_paths
8 |
9 |
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/Example/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - SwiftOverpassAPI (0.1.0)
3 |
4 | DEPENDENCIES:
5 | - SwiftOverpassAPI (from `../`)
6 |
7 | EXTERNAL SOURCES:
8 | SwiftOverpassAPI:
9 | :path: "../"
10 |
11 | SPEC CHECKSUMS:
12 | SwiftOverpassAPI: 5c393c758ed4e6f2ac5092b013a06a153f96f35e
13 |
14 | PODFILE CHECKSUM: eea53c5aff4448ac6bc2a434b559f295f29b3453
15 |
16 | COCOAPODS: 1.8.4
17 |
--------------------------------------------------------------------------------
/Example/Pods/Local Podspecs/SwiftOverpassAPI.podspec.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SwiftOverpassAPI",
3 | "version": "0.1.0",
4 | "summary": "A short description of SwiftOverpassAPI.",
5 | "description": "TODO: Add long description of the pod here.",
6 | "homepage": "https://github.com/ebsamson3/SwiftOverpassAPI",
7 | "license": {
8 | "type": "MIT",
9 | "file": "LICENSE"
10 | },
11 | "authors": {
12 | "ebsamson3": "ebsamson3@gmail.com"
13 | },
14 | "source": {
15 | "git": "https://github.com/ebsamson3/SwiftOverpassAPI.git",
16 | "tag": "0.1.0"
17 | },
18 | "platforms": {
19 | "ios": "8.0"
20 | },
21 | "source_files": "SwiftOverpassAPI/Classes/**/*"
22 | }
23 |
--------------------------------------------------------------------------------
/Example/Pods/Manifest.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - SwiftOverpassAPI (0.1.0)
3 |
4 | DEPENDENCIES:
5 | - SwiftOverpassAPI (from `../`)
6 |
7 | EXTERNAL SOURCES:
8 | SwiftOverpassAPI:
9 | :path: "../"
10 |
11 | SPEC CHECKSUMS:
12 | SwiftOverpassAPI: 5c393c758ed4e6f2ac5092b013a06a153f96f35e
13 |
14 | PODFILE CHECKSUM: eea53c5aff4448ac6bc2a434b559f295f29b3453
15 |
16 | COCOAPODS: 1.8.4
17 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | ${PRODUCT_BUNDLE_IDENTIFIER}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | ${PRODUCT_NAME}
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | ${CURRENT_PROJECT_VERSION}
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example-acknowledgements.markdown:
--------------------------------------------------------------------------------
1 | # Acknowledgements
2 | This application makes use of the following third party libraries:
3 |
4 | ## SwiftOverpassAPI
5 |
6 | Copyright (c) 2019 ebsamson3
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in
16 | all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | THE SOFTWARE.
25 |
26 | Generated by CocoaPods - https://cocoapods.org
27 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example-acknowledgements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreferenceSpecifiers
6 |
7 |
8 | FooterText
9 | This application makes use of the following third party libraries:
10 | Title
11 | Acknowledgements
12 | Type
13 | PSGroupSpecifier
14 |
15 |
16 | FooterText
17 | Copyright (c) 2019 ebsamson3 <ebsamson3@gmail.com>
18 |
19 | Permission is hereby granted, free of charge, to any person obtaining a copy
20 | of this software and associated documentation files (the "Software"), to deal
21 | in the Software without restriction, including without limitation the rights
22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23 | copies of the Software, and to permit persons to whom the Software is
24 | furnished to do so, subject to the following conditions:
25 |
26 | The above copyright notice and this permission notice shall be included in
27 | all copies or substantial portions of the Software.
28 |
29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35 | THE SOFTWARE.
36 |
37 | License
38 | MIT
39 | Title
40 | SwiftOverpassAPI
41 | Type
42 | PSGroupSpecifier
43 |
44 |
45 | FooterText
46 | Generated by CocoaPods - https://cocoapods.org
47 | Title
48 |
49 | Type
50 | PSGroupSpecifier
51 |
52 |
53 | StringsTable
54 | Acknowledgements
55 | Title
56 | Acknowledgements
57 |
58 |
59 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example-dummy.m:
--------------------------------------------------------------------------------
1 | #import
2 | @interface PodsDummy_Pods_SwiftOverpassAPI_Example : NSObject
3 | @end
4 | @implementation PodsDummy_Pods_SwiftOverpassAPI_Example
5 | @end
6 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example-frameworks.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | set -u
4 | set -o pipefail
5 |
6 | function on_error {
7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
8 | }
9 | trap 'on_error $LINENO' ERR
10 |
11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then
12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy
13 | # frameworks to, so exit 0 (signalling the script phase was successful).
14 | exit 0
15 | fi
16 |
17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
19 |
20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}"
21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}"
22 |
23 | # Used as a return value for each invocation of `strip_invalid_archs` function.
24 | STRIP_BINARY_RETVAL=0
25 |
26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution
27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
29 |
30 | # Copies and strips a vendored framework
31 | install_framework()
32 | {
33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
34 | local source="${BUILT_PRODUCTS_DIR}/$1"
35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
37 | elif [ -r "$1" ]; then
38 | local source="$1"
39 | fi
40 |
41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
42 |
43 | if [ -L "${source}" ]; then
44 | echo "Symlinked..."
45 | source="$(readlink "${source}")"
46 | fi
47 |
48 | # Use filter instead of exclude so missing patterns don't throw errors.
49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\""
50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}"
51 |
52 | local basename
53 | basename="$(basename -s .framework "$1")"
54 | binary="${destination}/${basename}.framework/${basename}"
55 |
56 | if ! [ -r "$binary" ]; then
57 | binary="${destination}/${basename}"
58 | elif [ -L "${binary}" ]; then
59 | echo "Destination binary is symlinked..."
60 | dirname="$(dirname "${binary}")"
61 | binary="${dirname}/$(readlink "${binary}")"
62 | fi
63 |
64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device
65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then
66 | strip_invalid_archs "$binary"
67 | fi
68 |
69 | # Resign the code if required by the build settings to avoid unstable apps
70 | code_sign_if_enabled "${destination}/$(basename "$1")"
71 |
72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7.
73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then
74 | local swift_runtime_libs
75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u)
76 | for lib in $swift_runtime_libs; do
77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
79 | code_sign_if_enabled "${destination}/${lib}"
80 | done
81 | fi
82 | }
83 |
84 | # Copies and strips a vendored dSYM
85 | install_dsym() {
86 | local source="$1"
87 | if [ -r "$source" ]; then
88 | # Copy the dSYM into a the targets temp dir.
89 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\""
90 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}"
91 |
92 | local basename
93 | basename="$(basename -s .framework.dSYM "$source")"
94 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}"
95 |
96 | # Strip invalid architectures so "fat" simulator / device frameworks work on device
97 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then
98 | strip_invalid_archs "$binary"
99 | fi
100 |
101 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then
102 | # Move the stripped file into its final destination.
103 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\""
104 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}"
105 | else
106 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing.
107 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM"
108 | fi
109 | fi
110 | }
111 |
112 | # Copies the bcsymbolmap files of a vendored framework
113 | install_bcsymbolmap() {
114 | local bcsymbolmap_path="$1"
115 | local destination="${BUILT_PRODUCTS_DIR}"
116 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}""
117 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"
118 | }
119 |
120 | # Signs a framework with the provided identity
121 | code_sign_if_enabled() {
122 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then
123 | # Use the current code_sign_identity
124 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
125 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'"
126 |
127 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
128 | code_sign_cmd="$code_sign_cmd &"
129 | fi
130 | echo "$code_sign_cmd"
131 | eval "$code_sign_cmd"
132 | fi
133 | }
134 |
135 | # Strip invalid architectures
136 | strip_invalid_archs() {
137 | binary="$1"
138 | # Get architectures for current target binary
139 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)"
140 | # Intersect them with the architectures we are building for
141 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)"
142 | # If there are no archs supported by this binary then warn the user
143 | if [[ -z "$intersected_archs" ]]; then
144 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)."
145 | STRIP_BINARY_RETVAL=0
146 | return
147 | fi
148 | stripped=""
149 | for arch in $binary_archs; do
150 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then
151 | # Strip non-valid architectures in-place
152 | lipo -remove "$arch" -output "$binary" "$binary"
153 | stripped="$stripped $arch"
154 | fi
155 | done
156 | if [[ "$stripped" ]]; then
157 | echo "Stripped $binary of architectures:$stripped"
158 | fi
159 | STRIP_BINARY_RETVAL=1
160 | }
161 |
162 |
163 | if [[ "$CONFIGURATION" == "Debug" ]]; then
164 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftOverpassAPI/SwiftOverpassAPI.framework"
165 | fi
166 | if [[ "$CONFIGURATION" == "Release" ]]; then
167 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftOverpassAPI/SwiftOverpassAPI.framework"
168 | fi
169 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
170 | wait
171 | fi
172 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example-umbrella.h:
--------------------------------------------------------------------------------
1 | #ifdef __OBJC__
2 | #import
3 | #else
4 | #ifndef FOUNDATION_EXPORT
5 | #if defined(__cplusplus)
6 | #define FOUNDATION_EXPORT extern "C"
7 | #else
8 | #define FOUNDATION_EXPORT extern
9 | #endif
10 | #endif
11 | #endif
12 |
13 |
14 | FOUNDATION_EXPORT double Pods_SwiftOverpassAPI_ExampleVersionNumber;
15 | FOUNDATION_EXPORT const unsigned char Pods_SwiftOverpassAPI_ExampleVersionString[];
16 |
17 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example.debug.xcconfig:
--------------------------------------------------------------------------------
1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI"
3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI/SwiftOverpassAPI.framework/Headers"
5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
6 | OTHER_LDFLAGS = $(inherited) -framework "SwiftOverpassAPI"
7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
8 | PODS_BUILD_DIR = ${BUILD_DIR}
9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
11 | PODS_ROOT = ${SRCROOT}/Pods
12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
13 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example.modulemap:
--------------------------------------------------------------------------------
1 | framework module Pods_SwiftOverpassAPI_Example {
2 | umbrella header "Pods-SwiftOverpassAPI_Example-umbrella.h"
3 |
4 | export *
5 | module * { export * }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Example/Pods-SwiftOverpassAPI_Example.release.xcconfig:
--------------------------------------------------------------------------------
1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI"
3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI/SwiftOverpassAPI.framework/Headers"
5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
6 | OTHER_LDFLAGS = $(inherited) -framework "SwiftOverpassAPI"
7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
8 | PODS_BUILD_DIR = ${BUILD_DIR}
9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
11 | PODS_ROOT = ${SRCROOT}/Pods
12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
13 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | ${PRODUCT_BUNDLE_IDENTIFIER}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | ${PRODUCT_NAME}
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | ${CURRENT_PROJECT_VERSION}
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests-acknowledgements.markdown:
--------------------------------------------------------------------------------
1 | # Acknowledgements
2 | This application makes use of the following third party libraries:
3 | Generated by CocoaPods - https://cocoapods.org
4 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests-acknowledgements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreferenceSpecifiers
6 |
7 |
8 | FooterText
9 | This application makes use of the following third party libraries:
10 | Title
11 | Acknowledgements
12 | Type
13 | PSGroupSpecifier
14 |
15 |
16 | FooterText
17 | Generated by CocoaPods - https://cocoapods.org
18 | Title
19 |
20 | Type
21 | PSGroupSpecifier
22 |
23 |
24 | StringsTable
25 | Acknowledgements
26 | Title
27 | Acknowledgements
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests-dummy.m:
--------------------------------------------------------------------------------
1 | #import
2 | @interface PodsDummy_Pods_SwiftOverpassAPI_Tests : NSObject
3 | @end
4 | @implementation PodsDummy_Pods_SwiftOverpassAPI_Tests
5 | @end
6 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests-umbrella.h:
--------------------------------------------------------------------------------
1 | #ifdef __OBJC__
2 | #import
3 | #else
4 | #ifndef FOUNDATION_EXPORT
5 | #if defined(__cplusplus)
6 | #define FOUNDATION_EXPORT extern "C"
7 | #else
8 | #define FOUNDATION_EXPORT extern
9 | #endif
10 | #endif
11 | #endif
12 |
13 |
14 | FOUNDATION_EXPORT double Pods_SwiftOverpassAPI_TestsVersionNumber;
15 | FOUNDATION_EXPORT const unsigned char Pods_SwiftOverpassAPI_TestsVersionString[];
16 |
17 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests.debug.xcconfig:
--------------------------------------------------------------------------------
1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI"
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI/SwiftOverpassAPI.framework/Headers"
4 | OTHER_LDFLAGS = $(inherited) -framework "SwiftOverpassAPI"
5 | PODS_BUILD_DIR = ${BUILD_DIR}
6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
8 | PODS_ROOT = ${SRCROOT}/Pods
9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
10 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests.modulemap:
--------------------------------------------------------------------------------
1 | framework module Pods_SwiftOverpassAPI_Tests {
2 | umbrella header "Pods-SwiftOverpassAPI_Tests-umbrella.h"
3 |
4 | export *
5 | module * { export * }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/Pods-SwiftOverpassAPI_Tests/Pods-SwiftOverpassAPI_Tests.release.xcconfig:
--------------------------------------------------------------------------------
1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI"
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI/SwiftOverpassAPI.framework/Headers"
4 | OTHER_LDFLAGS = $(inherited) -framework "SwiftOverpassAPI"
5 | PODS_BUILD_DIR = ${BUILD_DIR}
6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
8 | PODS_ROOT = ${SRCROOT}/Pods
9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
10 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/SwiftOverpassAPI/SwiftOverpassAPI-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | ${PRODUCT_BUNDLE_IDENTIFIER}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | ${PRODUCT_NAME}
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 0.1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | ${CURRENT_PROJECT_VERSION}
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/SwiftOverpassAPI/SwiftOverpassAPI-dummy.m:
--------------------------------------------------------------------------------
1 | #import
2 | @interface PodsDummy_SwiftOverpassAPI : NSObject
3 | @end
4 | @implementation PodsDummy_SwiftOverpassAPI
5 | @end
6 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/SwiftOverpassAPI/SwiftOverpassAPI-prefix.pch:
--------------------------------------------------------------------------------
1 | #ifdef __OBJC__
2 | #import
3 | #else
4 | #ifndef FOUNDATION_EXPORT
5 | #if defined(__cplusplus)
6 | #define FOUNDATION_EXPORT extern "C"
7 | #else
8 | #define FOUNDATION_EXPORT extern
9 | #endif
10 | #endif
11 | #endif
12 |
13 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/SwiftOverpassAPI/SwiftOverpassAPI-umbrella.h:
--------------------------------------------------------------------------------
1 | #ifdef __OBJC__
2 | #import
3 | #else
4 | #ifndef FOUNDATION_EXPORT
5 | #if defined(__cplusplus)
6 | #define FOUNDATION_EXPORT extern "C"
7 | #else
8 | #define FOUNDATION_EXPORT extern
9 | #endif
10 | #endif
11 | #endif
12 |
13 |
14 | FOUNDATION_EXPORT double SwiftOverpassAPIVersionNumber;
15 | FOUNDATION_EXPORT const unsigned char SwiftOverpassAPIVersionString[];
16 |
17 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/SwiftOverpassAPI/SwiftOverpassAPI.modulemap:
--------------------------------------------------------------------------------
1 | framework module SwiftOverpassAPI {
2 | umbrella header "SwiftOverpassAPI-umbrella.h"
3 |
4 | export *
5 | module * { export * }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Pods/Target Support Files/SwiftOverpassAPI/SwiftOverpassAPI.xcconfig:
--------------------------------------------------------------------------------
1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftOverpassAPI
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
4 | PODS_BUILD_DIR = ${BUILD_DIR}
5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
6 | PODS_ROOT = ${SRCROOT}
7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../..
8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
9 | SKIP_INSTALL = YES
10 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
11 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI.xcodeproj/xcshareddata/xcschemes/SwiftOverpassAPI-Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
51 |
52 |
53 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
76 |
78 |
84 |
85 |
86 |
87 |
93 |
95 |
101 |
102 |
103 |
104 |
106 |
107 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by ebsamson3 on 10/19/2019.
6 | // Copyright (c) 2019 ebsamson3. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 | var navigationCoordinator: OverpassDemoCoordinator?
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 |
19 | window = UIWindow(frame: UIScreen.main.bounds)
20 | window?.makeKeyAndVisible()
21 | let navigationController = UINavigationController()
22 | navigationCoordinator = OverpassDemoCoordinator(
23 | navigationController: navigationController)
24 |
25 | navigationCoordinator?.start()
26 |
27 | window?.rootViewController = navigationController
28 |
29 | return true
30 | }
31 |
32 | func applicationWillResignActive(_ application: UIApplication) {
33 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
34 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
35 | }
36 |
37 | func applicationDidEnterBackground(_ application: UIApplication) {
38 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
39 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
40 | }
41 |
42 | func applicationWillEnterForeground(_ application: UIApplication) {
43 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
44 | }
45 |
46 | func applicationDidBecomeActive(_ application: UIApplication) {
47 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
48 | }
49 |
50 | func applicationWillTerminate(_ application: UIApplication) {
51 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
52 | }
53 |
54 |
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/DemoMapViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoMapViewModel.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/11/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 | import SwiftOverpassAPI
11 |
12 | class DemoMapViewModel: NSObject, MapViewModel {
13 |
14 | // All MapKit Overpass Visualizations
15 | var visualizations = [Int: OPMapKitVisualization]()
16 |
17 | var annotations = [MKAnnotation]() // Annotations generated from center visualizations
18 | var overlays = [MKOverlay]() // Overlays generated from polygon/polyline type visualizations.
19 |
20 | // Variable for storing/setting the bound mapView's region
21 | var region: MKCoordinateRegion? {
22 | didSet {
23 | guard let region = region else { return }
24 | setRegion?(region)
25 | }
26 | }
27 |
28 | // Reuse identifier for marker annotation views.
29 | private let markerReuseIdentifier = "MarkerAnnotationView"
30 |
31 | // Handler functions for set the bound mapView's region and adding/removing annotations and overlays
32 | var setRegion: ((MKCoordinateRegion) -> Void)?
33 | var addAnnotations: (([MKAnnotation]) -> Void)?
34 | var addOverlays: (([MKOverlay]) -> Void)?
35 | var removeAnnotations: (([MKAnnotation]) -> Void)?
36 | var removeOverlays: (([MKOverlay]) -> Void)?
37 |
38 | // Function to register all reusable annotation views to the mapView
39 | func registerAnnotationViews(to mapView: MKMapView) {
40 | mapView.register(
41 | MKMarkerAnnotationView.self,
42 | forAnnotationViewWithReuseIdentifier: markerReuseIdentifier)
43 | }
44 |
45 | // Convert visualizations to MapKit overlays and annoations
46 | func addVisualizations(_ visualizations: [Int: OPMapKitVisualization]) {
47 |
48 | self.visualizations = visualizations
49 |
50 | removeAnnotations?(annotations)
51 | removeOverlays?(overlays)
52 | annotations = []
53 | overlays = []
54 |
55 | var newAnnotations = [MKAnnotation]()
56 | var polylines = [MKPolyline]()
57 | var polygons = [MKPolygon]()
58 |
59 | // For each visualization, append it to annotations, polylines, or polygons array depending on it's type.
60 | for visualization in visualizations.values {
61 | switch visualization {
62 | case .annotation(let annotation):
63 | newAnnotations.append(annotation)
64 | case .polyline(let polyline):
65 | polylines.append(polyline)
66 | case .polylines(let newPolylines):
67 | polylines.append(contentsOf: newPolylines)
68 | case .polygon(let polygon):
69 | polygons.append(polygon)
70 | case .polygons(let newPolygons):
71 | polygons.append(contentsOf: newPolygons)
72 | }
73 | }
74 |
75 | // Create MultiPolygon and Multipolyline overlays for rendering all the polylgons and polylines respectively. This allows each polygon and polyline to share a renderer so that they can be efficiently displayed on the mapView.
76 | let multiPolyline = MKMultiPolyline(polylines)
77 | let multiPolygon = MKMultiPolygon(polygons)
78 |
79 | // Create an overlays array from the multiPolyline and multiPolygon
80 | let newOverlays: [MKOverlay] = [multiPolyline, multiPolygon]
81 |
82 | // Store the new annotations and overlays in their respective variables
83 | annotations = newAnnotations
84 | overlays = newOverlays
85 |
86 | // Add the annotaitons and overlays to the mapView
87 | addAnnotations?(annotations)
88 | addOverlays?(overlays)
89 | }
90 |
91 | // Function called to center the mapView on a particular visualization
92 | func centerMap(onVisualizationWithId id: Int) {
93 | guard let visualization = visualizations[id] else {
94 | return
95 | }
96 |
97 | let region: MKCoordinateRegion
98 | let insetRatio: Double = -0.25
99 |
100 | let boundingRects: [MKMapRect]
101 |
102 | // If the visualization is an annotation then center on the annotation's coordinate. Otherwise, find the bounding rectangles of every object in the visualization
103 | switch visualization {
104 | case .annotation(let annotation):
105 | region = MKCoordinateRegion(
106 | center: annotation.coordinate,
107 | latitudinalMeters: 500,
108 | longitudinalMeters: 500)
109 | self.region = region
110 | return
111 | case .polyline(let polyline):
112 | boundingRects = [polyline.boundingMapRect]
113 | case .polygon(let polygon):
114 | boundingRects = [polygon.boundingMapRect]
115 | case .polylines(let polylines):
116 | boundingRects = polylines.map { $0.boundingMapRect }
117 | case .polygons(let polygons):
118 | boundingRects = polygons.map { $0.boundingMapRect }
119 | }
120 |
121 | // Find a larger rectable that encompasses all the bounding rectangles for each individual object in the visualization.
122 | guard
123 | let minX = (boundingRects.map { $0.minX }).min(),
124 | let maxX = (boundingRects.map { $0.maxX }).max(),
125 | let minY = (boundingRects.map { $0.minY }).min(),
126 | let maxY = (boundingRects.map { $0.maxY }).max()
127 | else {
128 | return
129 | }
130 | let width = maxX - minX
131 | let height = maxY - minY
132 | let rect = MKMapRect(x: minX, y: minY, width: width, height: height)
133 |
134 | // Pad the large rectangle by the specified ratio
135 | let paddedRect = rect.insetBy(dx: width * insetRatio, dy: height * insetRatio)
136 |
137 | // Convert the rectangle to a MKCoordinateRegion
138 | region = MKCoordinateRegion(paddedRect)
139 |
140 | // Set the mapView region to the new visualization-emcompassing region
141 | self.region = region
142 | }
143 |
144 | // Renderers for various overlay types
145 | func renderer(for overlay: MKOverlay) -> MKOverlayRenderer {
146 |
147 | let strokeWidth: CGFloat = 2
148 | let strokeColor = UIColor.theme
149 | let fillColor = UIColor.theme.withAlphaComponent(0.5)
150 |
151 | if let polyline = overlay as? MKPolyline {
152 | let renderer = MKPolylineRenderer(polyline: polyline)
153 | renderer.strokeColor = strokeColor
154 | renderer.lineWidth = strokeWidth
155 | return renderer
156 | } else if let polygon = overlay as? MKPolygon {
157 | let renderer = MKPolygonRenderer(polygon: polygon)
158 | renderer.fillColor = fillColor
159 | renderer.strokeColor = strokeColor
160 | renderer.lineWidth = strokeWidth
161 | return renderer
162 | } else if let multiPolyline = overlay as? MKMultiPolyline {
163 | let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolyline)
164 | renderer.strokeColor = strokeColor
165 | renderer.lineWidth = strokeWidth
166 | return renderer
167 | } else if let multiPolygon = overlay as? MKMultiPolygon {
168 | let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygon)
169 | renderer.fillColor = fillColor
170 | renderer.strokeColor = strokeColor
171 | renderer.lineWidth = strokeWidth
172 | return renderer
173 | } else {
174 | return MKOverlayRenderer()
175 | }
176 | }
177 |
178 | // Set the annotaiton view for annotations visualized on the mapView
179 | func view(for annotation: MKAnnotation) -> MKAnnotationView? {
180 | guard let pointAnnotation = annotation as? MKPointAnnotation else {
181 | return nil
182 | }
183 | let view = MKMarkerAnnotationView(
184 | annotation: pointAnnotation,
185 | reuseIdentifier: markerReuseIdentifier)
186 |
187 | view.markerTintColor = UIColor.theme
188 | return view
189 | }
190 |
191 | // If the user changes the region through a gesture, set the stored region to nil. This will stop the mapView from recentering itself when the edge insets change.
192 | func userDidGestureOnMapView(sender: UIGestureRecognizer) {
193 |
194 | if
195 | sender.isKind(of: UIPanGestureRecognizer.self) ||
196 | sender.isKind(of: UIPinchGestureRecognizer.self)
197 | {
198 | region = nil
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/DemoTableViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoTableViewModel.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/15/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftOverpassAPI
11 |
12 | protocol DemoTableViewModelDelegate: class {
13 | func didSelectCell(forElementWithId id: Int)
14 | }
15 |
16 | // A view model for a tableView that displays the names of Overpass elements
17 | class DemoTableViewModel: NSObject, TableViewModel {
18 |
19 | // A simple selectable cell view model class
20 | private var cellViewModels = [SelectableCellViewModel]()
21 |
22 | // An array holding all relavant cellViewModel types. Cell representable is the protocol cellViewModels must conform to.
23 | private var cellViewModelTypes: [CellRepresentable.Type] = [
24 | SelectableCellViewModel.self
25 | ]
26 |
27 | // The handler that is called whenever the bound tableView needs to reload.
28 | var reloadData: (() -> Void)?
29 |
30 | weak var delegate: DemoTableViewModelDelegate?
31 |
32 | let demo: Demo
33 |
34 | init(demo: Demo) {
35 | self.demo = demo
36 | }
37 |
38 | // Register all relevant table view cell types using the static function in each cellViewModel type.
39 | func registerCells(tableView: UITableView) {
40 | for cellViewModelType in cellViewModelTypes {
41 | cellViewModelType.registerCell(tableView: tableView)
42 | }
43 | }
44 |
45 | func numberOfRows(inSection section: Int) -> Int {
46 | return cellViewModels.count
47 | }
48 |
49 | // Use the cellViewModel instances to generate the corresponding cell instances.
50 | func getCellViewModel(at indexPath: IndexPath) -> CellRepresentable {
51 | let row = indexPath.row
52 | return cellViewModels[row]
53 | }
54 |
55 | // Call the selection handler of the cellViewModel that corresponds to the selected cell.
56 | func handleCellSelection(at indexPath: IndexPath) {
57 | let row = indexPath.row
58 | cellViewModels[row].selectionHandler?()
59 | }
60 |
61 | // Generate cellViewModels for each element
62 | func generateCellViewModels(forElements elements: [Int: OPElement]) {
63 |
64 | // Perform cellViewModel generation off the main thread to keep UI responsive
65 | DispatchQueue.global(qos: .userInitiated).async { [weak self] in
66 |
67 | guard let demo = self?.demo else {
68 | return
69 | }
70 |
71 | // Untitled elements are given generic name followed by a unique number. We use a number formatter to format the number of digits in the number string.
72 | let numberFormatter = NumberFormatter()
73 | let minimumIntegerDigits = String(elements.count).count
74 | numberFormatter.minimumIntegerDigits = minimumIntegerDigits
75 |
76 | // Array that stores the cellViewModels that will be added
77 | var newCellViewModels = [SelectableCellViewModel]()
78 |
79 | var counter = 1
80 |
81 | // For each element
82 | for (id, element) in elements {
83 |
84 | // If the element is interesting and not skippable
85 | guard element.isInteresting && !element.isSkippable else {
86 | continue
87 | }
88 |
89 |
90 | let title: String
91 |
92 | if let elementName = element.tags["name"] {
93 | // If the name tage has a value, set the cell title to that name
94 | title = elementName
95 | } else {
96 |
97 | let counterValue = NSNumber(value: counter)
98 |
99 | guard
100 | let numberString = numberFormatter.string(from: counterValue)
101 | else {
102 | continue
103 | }
104 |
105 | // Otherwise use the generic name supplied by the demo object plus an incrementing number identifier
106 | title = "Untitled \(demo.resultUnit) \(numberString)"
107 | counter += 1
108 | }
109 |
110 | // Generate the cellViewModel
111 | let cellViewModel = SelectableCellViewModel(title: title)
112 |
113 | // Set the selection handler of the cellViewModel so that it calls the delegate method of the tableViewModel
114 | cellViewModel.selectionHandler = {
115 | self?.delegate?.didSelectCell(forElementWithId: id)
116 | }
117 |
118 | newCellViewModels.append(cellViewModel)
119 | }
120 |
121 | // Sort the cellViewModels by name
122 | newCellViewModels.sort { (viewModel1, viewModel2) in
123 | viewModel1.title < viewModel2.title
124 | }
125 |
126 | // Switch back to the main thread and set the cellViewModels array to the new cellViewModels. Notify the tableView that it should reload.
127 | DispatchQueue.main.async {
128 | self?.cellViewModels = newCellViewModels
129 | self?.reloadData?()
130 | }
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/DemoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoViewController.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MapKit
11 |
12 | // Main view controller for running various overpass API demos
13 | class DemoViewController: UIViewController {
14 |
15 | // Max and min heights above for the pull up container view while the device is in portrait orientation
16 | lazy var minPortraitHeight: CGFloat = 44
17 | lazy var maxPortraitHeight: CGFloat = {
18 | let bounds = UIScreen.main.bounds
19 | let maxDimension = max(bounds.width, bounds.height)
20 | return maxDimension * 0.4
21 | }()
22 |
23 | // Frame of the pull up container while the device is in landscape orientation.
24 | let landscapeFrame = CGRect(x: 16, y: 16, width: 250, height: 300)
25 |
26 |
27 | // DemoViewController has two main child view controllers, a MapViewController and a TableViewController. This is mirrored by the DemoViewController's view model, which has two child view models: a MapViewModel and a TableViewModel.
28 | private lazy var mapViewController = MapViewController(
29 | viewModel: viewModel.mapViewModel
30 | )
31 | private lazy var tableViewController = TableViewController(
32 | viewModel: viewModel.tableViewModel)
33 |
34 | // The table view controller is placed inside of a custom container view controller that enables the tableview to be pulled up from the bottom of the screen.
35 | private lazy var pullUpContainer: PullUpContainer = {
36 |
37 | // Embed the tableViewController inside the pull up controller
38 | let pullUpContainer = PullUpContainer(
39 | contentViewController: tableViewController)
40 |
41 | // Configuring the pull up container's dimensions after initialization
42 | pullUpContainer.headerViewHeight = minPortraitHeight
43 | pullUpContainer.minPortraitHeight = minPortraitHeight
44 | pullUpContainer.maxPortraitHeight = maxPortraitHeight
45 | pullUpContainer.landscapeFrame = landscapeFrame
46 |
47 | // Setting the pull up container delegate to the DemoViewController
48 | pullUpContainer.delegate = self
49 |
50 | // Return the pull up container after it has been configured
51 | return pullUpContainer
52 | }()
53 |
54 | // A spinner for indicating that an Overpass query is in progress
55 | private let spinner: UIActivityIndicatorView = {
56 | let spinner = UIActivityIndicatorView()
57 | spinner.hidesWhenStopped = true
58 | spinner.style = .large
59 | return spinner
60 | }()
61 |
62 | // While loading, change the navigation bar title to "Fetching results..."
63 | private let loadingLabel: UILabel = {
64 | let label = UILabel()
65 | label.text = "Fetching results..."
66 | return label
67 | }()
68 |
69 | // Button for resetting the mapView's region
70 | private lazy var resetMapViewButton = UIBarButtonItem(
71 | title: "Reset",
72 | style: UIBarButtonItem.Style.plain,
73 | target: self, action: #selector(resetMapViewRegion(sender:)))
74 |
75 | // The DemoViewController's main view model
76 | let viewModel: DemoViewModel
77 |
78 | // Initialize the view controller with a demo view model
79 | init(viewModel: DemoViewModel) {
80 | self.viewModel = viewModel
81 | super.init(nibName: nil, bundle: nil)
82 |
83 | // Connecting the navigation bar title and activity spinner animation to the loading status of the view model
84 | viewModel.loadingStatusDidChangeTo = { [weak self] isLoading in
85 | if isLoading {
86 | self?.navigationItem.titleView = self?.loadingLabel
87 | self?.loadingLabel.sizeToFit()
88 | self?.spinner.startAnimating()
89 | } else {
90 | self?.navigationItem.titleView = nil
91 | self?.spinner.stopAnimating()
92 | }
93 | }
94 |
95 | // After configuring the view model we run it.
96 | viewModel.run()
97 | }
98 |
99 | // Required boilerplate
100 | required init?(coder: NSCoder) {
101 | fatalError("init(coder:) has not been implemented")
102 | }
103 |
104 | // Configure the view constraints on view did load
105 | override func viewDidLoad() {
106 | super.viewDidLoad()
107 | configure()
108 | }
109 |
110 | // Once the view has appeared, run pullUpContainer(statudDidChange:) to adjust the map view content so that it isn't covered by the pull up container.
111 | override func viewDidAppear(_ animated: Bool) {
112 | super.viewDidAppear(animated)
113 |
114 | guard let status = pullUpContainer.status else {
115 | return
116 | }
117 | pullUpContainer(statusDidChangeTo: status)
118 | }
119 |
120 | private func configure() {
121 | // Add the mapViewController as a child view controller to demoViewController
122 | addChild(mapViewController)
123 | mapViewController.didMove(toParent: self)
124 |
125 | // Calls the viewDidLoad() method of the mapViewController
126 | _ = mapViewController.view
127 |
128 | // Add the view of the mapViewControlelr as a subview to the demoViewController's view and configure it's constraints.
129 | let mapView = mapViewController.mapView
130 | view.addSubview(mapView)
131 | mapViewController.view.translatesAutoresizingMaskIntoConstraints = false
132 | mapView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
133 | mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
134 | mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
135 | mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
136 |
137 | // Add button for reseting mapView to region to default query region
138 | navigationItem.rightBarButtonItem = resetMapViewButton
139 |
140 | // Add the pull up container as a child view controller to the demoViewController
141 | addPullUpContainer(pullUpContainer)
142 |
143 | // Lastly, add the spinner as a subview to the demoViewController's view and setup it's constraints
144 | view.addSubview(spinner)
145 | spinner.translatesAutoresizingMaskIntoConstraints = false
146 | spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
147 | spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
148 | }
149 |
150 | // Handle resetting the mapView region
151 | @objc private func resetMapViewRegion(sender: UIButton) {
152 | viewModel.resetMapViewRegion()
153 | }
154 | }
155 |
156 | extension DemoViewController: PullUpContainerDelegate {
157 |
158 | // Called whenever the pullUpContainer's frame changes. Adjusts the mapView's content so that it is centered on the portion of the mapVeiw that isn't covered by the pullUpContainer.
159 | func pullUpContainer(statusDidChangeTo status: PullUpContainer.Status) {
160 | switch status {
161 | case .portrait(let height):
162 | if height == maxPortraitHeight || height == minPortraitHeight {
163 | let edgeInsets = UIEdgeInsets(
164 | top: 0,
165 | left: 0,
166 | bottom: height - view.safeAreaInsets.bottom,
167 | right: 0)
168 | mapViewController.setEdgeInsets(to: edgeInsets)
169 | }
170 | case .landscape:
171 | let edgeInsets = UIEdgeInsets(
172 | top: 0,
173 | left: landscapeFrame.origin.x + landscapeFrame.width + view.layoutMargins.left,
174 | bottom: 0,
175 | right: 0)
176 | mapViewController.setEdgeInsets(to: edgeInsets)
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/DemoViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoViewModel.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 | import SwiftOverpassAPI
11 |
12 | class DemoViewModel {
13 |
14 | let demo: Demo // Contains Overpass query details
15 | let overpassClient: OPClient // The client for requesting/decoding Overpass data
16 |
17 | // Overpass request did start/finish
18 | var loadingStatusDidChangeTo: ((_ isLoading: Bool) -> Void)?
19 |
20 | var elements = [Int: OPElement]() // Elements returned by an Overpass request
21 |
22 | // Configures Overpass visualizations for mapKit display
23 | let mapViewModel = DemoMapViewModel()
24 |
25 | // Configures Overpass elements for tableView display
26 | lazy var tableViewModel: DemoTableViewModel = {
27 | let tableViewModel = DemoTableViewModel(demo: demo)
28 | tableViewModel.delegate = self
29 | return tableViewModel
30 | }()
31 |
32 | // DemoViewModel is initialized with an overpass client and a demo that contains specific Overpass query details.
33 | init(demo: Demo, overpassClient: OPClient) {
34 | self.demo = demo
35 | self.overpassClient = overpassClient
36 | }
37 |
38 | // Run the query generated by the demo
39 | func run() {
40 |
41 | // Set the laoding status to true
42 | loadingStatusDidChangeTo?(true)
43 |
44 | // The square geographic region in which results will be requested
45 | let region = demo.defaultRegion
46 |
47 | // Set the mapView to the correct region for displaying the query
48 | setVisualRegion(forQueryRegion: region)
49 |
50 | // Generate the overpass query from the demo
51 | let query = demo.generateQuery(forRegion: region)
52 |
53 | // Post the query to the Overpass endpoint. The endpoint will send a response containing the matching elements.
54 | overpassClient.fetchElements(query: query) { result in
55 |
56 | switch result {
57 | case .failure(let error):
58 | print(error.localizedDescription)
59 | case .success(let elements):
60 |
61 | // Store the decoded elements
62 | self.elements = elements
63 |
64 | // Use the tableViewModel to create cell view models for the returned elements
65 | self.tableViewModel.generateCellViewModels(forElements: elements)
66 |
67 | // Generate mapKit visualizations for the returned elements using a static visualization generator
68 | let visualizations = OPVisualizationGenerator
69 | .mapKitVisualizations(forElements: elements)
70 |
71 | // Add the generated visualizations to the mapView via the mapViewModel
72 | self.mapViewModel.addVisualizations(visualizations)
73 | }
74 | // Set the loading status to false
75 | self.loadingStatusDidChangeTo?(false)
76 | }
77 | }
78 |
79 | // Inset the query region to get the region that will be displayed on the mapView. This prevents query boundaries from being visible when the query results are first displayed.
80 | private func setVisualRegion(forQueryRegion region: MKCoordinateRegion) {
81 | let queryRect = region.toMKMapRect()
82 | let visualRect = queryRect.insetBy(dx: queryRect.width * 0.25, dy: queryRect.height * 0.25)
83 | let visualRegion = MKCoordinateRegion(visualRect)
84 | mapViewModel.region = visualRegion
85 | }
86 |
87 | func resetMapViewRegion() {
88 | setVisualRegion(forQueryRegion: demo.defaultRegion)
89 | }
90 | }
91 |
92 | extension DemoViewModel: DemoTableViewModelDelegate {
93 |
94 | // If the user selects a cell, center the mapView on that cell's element
95 | func didSelectCell(forElementWithId id: Int) {
96 | mapViewModel.centerMap(onVisualizationWithId: id)
97 |
98 | if let tags = elements[id]?.tags {
99 | print("Selected element with tags: \(tags)")
100 | }
101 |
102 | if let meta = elements[id]?.meta {
103 | print("Meta data: \(meta)")
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/MapViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapViewController.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/10/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MapKit
11 |
12 | // A basic MVVM MapViewController
13 | class MapViewController: UIViewController {
14 |
15 | // If a portion of the mapView is obscured we can set edge insets to move the mapView content to the mapView's visible area.
16 | private var edgeInsets: UIEdgeInsets?
17 |
18 | // Initializing the mapView and setting the controller to be it's delegate
19 | lazy var mapView: MKMapView = {
20 | let mapView = MKMapView()
21 | mapView.delegate = self
22 | return mapView
23 | }()
24 |
25 | // The view model that governs what content the mapView displays
26 | let viewModel: MapViewModel
27 |
28 | // Initialize with a view model
29 | init(viewModel: MapViewModel) {
30 | self.viewModel = viewModel
31 | super.init(nibName: nil, bundle: nil)
32 |
33 |
34 | // After initialization, add pan, pinch, and rotation gestures so that we can notify the view model whenever the user physically interacts with the mapView.
35 | let panGesureRecognizer = UIPanGestureRecognizer.init(
36 | target: self,
37 | action: #selector(userDidGestureOnMapView(sender:)))
38 |
39 | panGesureRecognizer.delegate = self
40 |
41 | let pinchGestureRecognizer = UIPinchGestureRecognizer.init(
42 | target: self,
43 | action: #selector(userDidGestureOnMapView(sender:)))
44 |
45 | pinchGestureRecognizer.delegate = self
46 |
47 | let rotationGestureRecognizer = UIRotationGestureRecognizer.init(
48 | target: self,
49 | action: #selector(userDidGestureOnMapView(sender:)))
50 |
51 | rotationGestureRecognizer.delegate = self
52 |
53 | mapView.addGestureRecognizer(panGesureRecognizer)
54 | mapView.addGestureRecognizer(pinchGestureRecognizer)
55 | mapView.addGestureRecognizer(rotationGestureRecognizer)
56 | }
57 |
58 | required init?(coder: NSCoder) {
59 | fatalError("init(coder:) has not been implemented")
60 | }
61 |
62 | // On view did load we configure tha mapViewModel's handler functions
63 | override func viewDidLoad() {
64 | super.viewDidLoad()
65 |
66 | // Register all reusable annotatino views to the mapView
67 | viewModel.registerAnnotationViews(to: mapView)
68 |
69 | // Called whenever the mapView should set a new region
70 | viewModel.setRegion = { [weak self] region in
71 | self?.updateRegion(to: region)
72 | }
73 |
74 | // Called whenever the mapView should add annotations
75 | viewModel.addAnnotations = { [weak self] annotations in
76 | self?.mapView.addAnnotations(annotations)
77 | }
78 |
79 | // Called whenever the mapView should add overlays
80 | viewModel.addOverlays = { [weak self] overlays in
81 | self?.mapView.addOverlays(overlays)
82 | }
83 |
84 | // Called whenever the mapView should remove annotations
85 | viewModel.removeAnnotations = { [weak self] annotations in
86 | self?.mapView.removeAnnotations(annotations)
87 | }
88 |
89 | // Called whenever the mapView should remove overlays
90 | viewModel.removeOverlays = { [weak self] overlays in
91 | self?.mapView.removeOverlays(overlays)
92 | }
93 |
94 | // Set the current region specified by the viewModel.
95 | if let region = viewModel.region {
96 | mapView.setRegion(region, animated: true)
97 | }
98 |
99 | // Add all of the viewModel's annotations and overlays
100 | mapView.addAnnotations(viewModel.annotations)
101 | mapView.addOverlays(viewModel.overlays)
102 |
103 | // Configure the mapViewConstraints
104 | configure()
105 | }
106 |
107 | // Configure the mapView constraints
108 | private func configure() {
109 | view.addSubview(mapView)
110 | mapView.translatesAutoresizingMaskIntoConstraints = false
111 | mapView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
112 | mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
113 | mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
114 | mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
115 | }
116 |
117 | // Called whenever the mapView's edge insets need to change.
118 | func setEdgeInsets(to edgeInsets: UIEdgeInsets) {
119 | self.edgeInsets = edgeInsets
120 |
121 | guard let region = viewModel.region else {
122 | return
123 | }
124 |
125 | // After setting the new edge insets, update the mapView region
126 | updateRegion(to: region)
127 | }
128 |
129 | // Updating the mapView region
130 | private func updateRegion(to region: MKCoordinateRegion) {
131 |
132 | // If no edge insets are specified, simply set the mapView region using the standard mapView method
133 | guard let edgeInsets = edgeInsets else {
134 | mapView.setRegion(region, animated: true)
135 | return
136 | }
137 |
138 | // Otherwise, conver the region to a mapRect and pad the region with the edge insets.
139 | let rect = region.toMKMapRect()
140 |
141 | mapView.setVisibleMapRect(
142 | rect,
143 | edgePadding: edgeInsets,
144 | animated: true)
145 | }
146 |
147 | // Whenever the user gestures on the mapView, notify the viewModel and pass the gesture recognizer.
148 | @objc func userDidGestureOnMapView(sender: UIGestureRecognizer) {
149 | viewModel.userDidGestureOnMapView(sender: sender)
150 | }
151 | }
152 |
153 |
154 | extension MapViewController: MKMapViewDelegate {
155 | // Delegate method for rendering overlays
156 | func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
157 | return viewModel.renderer(for: overlay)
158 | }
159 |
160 | // Delegate method for setting annotation views.
161 | func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
162 | return viewModel.view(for: annotation)
163 | }
164 | }
165 |
166 | extension MapViewController: UIGestureRecognizerDelegate {
167 | // Gesture recognizer delegate method that allows two gesture recognizers to correspond to a single gesture. We do this so we can recognize the user gestures while the standard mapView gestures are still active.
168 | func gestureRecognizer(
169 | _ gestureRecognizer: UIGestureRecognizer,
170 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
171 | {
172 | return true
173 | }
174 | }
175 |
176 |
177 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/PullUpContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PullUpContainer.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/9/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol PullUpContainerDelegate: class {
12 | func pullUpContainer(statusDidChangeTo status: PullUpContainer.Status)
13 | }
14 |
15 | /*
16 | A container view controller that enables it's child content view controller to be pulled up from the bottom of the screen. It works for this app but I haven't rigorously tested it in many different use cases. Largely base on the following repo:
17 | https://github.com/MarioIannotta/PullUpController
18 | */
19 |
20 | class PullUpContainer: UIViewController {
21 |
22 | // The pull up container has two distinct state for when it the device is in portrait and landscape. In portrait mode it is swiped up and down from the bottom of the screen while in landscape mode it is a static square frame.
23 | enum Status {
24 | case portrait(height: CGFloat)
25 | case landscape
26 | }
27 |
28 | // A header view with a handle that can be used to pull up the container when the embedded view controller is fully hidden.
29 | let headerView: PullUpHeaderView = {
30 | let view = PullUpHeaderView()
31 | view.backgroundColor = .white
32 | return view
33 | }()
34 |
35 | // The contentView provides the frome for the embedded contentViewController
36 | private let contentView = UIView()
37 | private let contentViewController: UIViewController
38 |
39 | // The pull up container has two sets of contraints that govern it's frame. One for when the device is in portrait and one for when the device is in landscape. When switching between these two states, one set of contraints is deactivated before the other is activated.
40 | private var portraitConstraints = ConstraintFamily()
41 | private var landscapeConstraints = ConstraintFamily()
42 |
43 | // The height of the header view of the pull up container
44 | private var headerViewHeightConstraint: NSLayoutConstraint?
45 |
46 | // A status variable that returns whether or not the pull up container is in the portrait or landscape state. If it is in portrait the height is stored as an associated value.
47 | var status: Status? {
48 | if isPortrait {
49 | guard let height = portraitConstraints.top?.constant else {
50 | return nil
51 | }
52 | return .portrait(height: height)
53 | } else {
54 | return .landscape
55 | }
56 | }
57 |
58 | // A check for whether or not the device is in portrait or landscape orientation
59 | private var isPortrait: Bool {
60 | return UIScreen.main.bounds.height > UIScreen.main.bounds.width
61 | }
62 |
63 | // The frame for the pull up container while it is in landscape mode
64 | var landscapeFrame = CGRect.zero {
65 | didSet {
66 | updateLandscapeConstraints()
67 | }
68 | }
69 |
70 | // The height of the pull up container's header view.
71 | var headerViewHeight: CGFloat = 40 {
72 | didSet {
73 | headerViewHeightConstraint?.constant = headerViewHeight
74 | }
75 | }
76 |
77 | // Te maximum and minimum heights for the pull up container while in portrait mode
78 | lazy var maxPortraitHeight: CGFloat = headerViewHeight
79 | lazy var minPortraitHeight: CGFloat = headerViewHeight
80 | var bounceOffset: CGFloat = 20
81 |
82 | // The coner radius of the pull up container
83 | var cornerRadius: CGFloat = 16 {
84 | didSet {
85 | contentViewBottomConstraint.constant = contentOffset
86 | }
87 | }
88 |
89 | // The offset between the bottom anchors of the content view and the pull up container's view. In portrait mode the pull up container's bottom anchor is below that of the parent view it is attached to. This is to hide the rounded bottom left and bottom right corners. To compensate for this, the content view has to be shifted up an amount equal to the corner radius so that it's bottom is aligned with the parent view of the pull up container's bottom anchor.
90 | private var contentOffset: CGFloat {
91 | return isPortrait ? -cornerRadius : 0
92 | }
93 |
94 | // The contraint between the bottom anchor of the content view and the bottom anchor of the pull up container
95 | private lazy var contentViewBottomConstraint: NSLayoutConstraint = {
96 | let constraint = contentView.bottomAnchor.constraint(
97 | equalTo: view.bottomAnchor,
98 | constant: contentOffset)
99 | .withPriority(.defaultHigh)
100 | return constraint
101 | }()
102 |
103 | weak var delegate: PullUpContainerDelegate?
104 |
105 | // Initialize the pull up container with the embedded content view controller
106 | init(
107 | contentViewController: UIViewController)
108 | {
109 | self.contentViewController = contentViewController
110 | super.init(nibName: nil, bundle: nil)
111 | configure()
112 | }
113 |
114 | // Required boilerplate
115 | required init?(coder: NSCoder) {
116 | fatalError("init(coder:) has not been implemented")
117 | }
118 |
119 | // Handle configuration changes that occur as a result of view size changes
120 | override func viewWillTransition(
121 | to size: CGSize,
122 | with coordinator: UIViewControllerTransitionCoordinator)
123 | {
124 | // Check if device will be in portrait after the size change occurs
125 | let isNewSizePortrait = size.height > size.width
126 |
127 | // Check if the device will change orientation
128 | guard isNewSizePortrait != isPortrait else {
129 | return
130 | }
131 |
132 | // Hide the view prior to changing between portrait and landscape mode. I like this better than animating the contriant changes as it looks a lot cleaner.
133 | view.isHidden = true
134 |
135 | // Deactivate the currently active constraints.
136 | if isPortrait {
137 | portraitConstraints.deactivate()
138 | } else {
139 | landscapeConstraints.deactivate()
140 | }
141 |
142 | // Execuute the following after the size transition animation has completed
143 | coordinator.animate(alongsideTransition: { _ in }) { _ in
144 |
145 | // Activate new contraints and notify the delegate of the change in state
146 | if isNewSizePortrait {
147 | // If the new state is portrait, minimize the pull up container.
148 | let topConstant = -self.minPortraitHeight
149 | self.portraitConstraints.top?.constant = topConstant
150 | self.portraitConstraints.activate()
151 | self.delegate?.pullUpContainer(statusDidChangeTo: .portrait(height: -topConstant))
152 | } else {
153 | self.landscapeConstraints.activate()
154 | self.delegate?.pullUpContainer(statusDidChangeTo: .landscape)
155 | }
156 | // Set the content offset from the bottom of the pull up container.
157 | self.contentViewBottomConstraint.constant = self.contentOffset
158 |
159 | // Show the pull up container after all the configuration changes have finished.
160 | self.view.isHidden = false
161 | }
162 | }
163 |
164 | // Set the corner radius each time the view lays out it's subviews
165 | override func viewWillLayoutSubviews() {
166 | super.viewWillLayoutSubviews()
167 | view.layer.cornerRadius = cornerRadius
168 | }
169 |
170 | // Initial configuration of the pull up container
171 | private func configure() {
172 |
173 | // Set up the appearence of the pull up container's border
174 | view.clipsToBounds = true
175 | view.layer.borderColor = UIColor(white: 0.7, alpha: 1).cgColor
176 | view.layer.borderWidth = 1
177 |
178 | // Add a pan gesture to the pull up container
179 | addMainPanGestureRecognizer()
180 |
181 | // Add containts for the header view and content view of the pull up container
182 | configureSubviewConstraints()
183 |
184 | // Embedthe content view controller into the pull up container
185 | add(childContentViewController: contentViewController)
186 | }
187 |
188 | // Add a pan gesture to the pull up container that enabled the container to be swiped up and down
189 | private func addMainPanGestureRecognizer() {
190 | let panGestureRecognizer = UIPanGestureRecognizer(
191 | target: self,
192 | action: #selector(handlePan(_:)))
193 |
194 | panGestureRecognizer.minimumNumberOfTouches = 1
195 | panGestureRecognizer.maximumNumberOfTouches = 1
196 | view.addGestureRecognizer(panGestureRecognizer)
197 | }
198 |
199 | // Embed the content view controller into the pull up container
200 | private func add(childContentViewController contentViewController: UIViewController) {
201 | addChild(contentViewController)
202 | contentViewController.didMove(toParent: self)
203 |
204 | guard let viewToAdd = contentViewController.view else {
205 | return
206 | }
207 |
208 | // Add the content view controller's view as a subview to the content view
209 | contentView.addSubview(viewToAdd)
210 |
211 | // Configure constraints such that the content view controller's view covers the whole extent of the content view.
212 | viewToAdd.translatesAutoresizingMaskIntoConstraints = false
213 | viewToAdd.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
214 | viewToAdd.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
215 | viewToAdd.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
216 | viewToAdd.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
217 |
218 | // Configure any scroll view that are present in the content view controller
219 | configureInternalScrollViews()
220 | }
221 |
222 | private func configureInternalScrollViews() {
223 | // Check the content view controller's subviews for any scroll views
224 | for subview in contentViewController.view.subviews {
225 | guard let scrollView = subview as? UIScrollView else {
226 | continue
227 | }
228 |
229 | // Make sure any scroll view does not adjust it's content below the top anchor of the content view controller's view.
230 | scrollView.contentInsetAdjustmentBehavior = .never
231 |
232 | // Add pan gesture recognizers to each scroll view. This enabled the pull up/down action of the pull up container to work with scroll views.
233 | scrollView.panGestureRecognizer.addTarget(
234 | self,
235 | action: #selector(handleScrollViewPan(sender:)))
236 | }
237 | }
238 |
239 | // Add the pull up container to a parent view controller
240 | func add(toParent parent: UIViewController) {
241 |
242 | // remove the pull up container from the previous parent if it exists
243 | if let currentParent = self.parent {
244 | remove(fromParent: currentParent, animated: false)
245 | }
246 |
247 | // Add the pull up container as a child view controller to the new parent
248 | parent.addChild(self)
249 | didMove(toParent: parent)
250 | parent.view.addSubview(view)
251 |
252 | // Configure the constraints between the pull up container and it's parent view
253 | configureConstraints(withParent: parent)
254 | }
255 |
256 | // Remove the pull up container from it's parent view controller
257 | func remove(fromParent parent: UIViewController, animated: Bool) {
258 |
259 | // Collapse the pull up container if it is in portrait
260 | if isPortrait {
261 | portraitConstraints.top?.constant = 0
262 | }
263 |
264 | // Collapse the pull up container if it is in portrait. Otherwise, fade out the pull up container
265 | if animated {
266 | UIView.animate(
267 | withDuration: 0.3,
268 | animations: {
269 | if self.isPortrait {
270 | self.view.layoutIfNeeded()
271 | } else {
272 | self.view.alpha = 0.0
273 | }
274 | }) { _ in
275 | // Destroy any constraint between the pll up container and it's parent
276 | self.portraitConstraints = ConstraintFamily()
277 | self.portraitConstraints = ConstraintFamily()
278 |
279 | // Remove the pull up container as a child view controller
280 | self.willMove(toParent: nil)
281 | self.view.removeFromSuperview()
282 | self.removeFromParent()
283 |
284 | // Reset the alpha of the pull up container to 1.0
285 | self.view.alpha = 1.0
286 | }
287 | }
288 | }
289 |
290 | // Configure the portrait and landscape constraints between the pull up container and it's parent view controller
291 | private func configureConstraints(withParent parent: UIViewController) {
292 |
293 | guard let parentView = parent.view else {
294 | return
295 | }
296 |
297 | let margins = parentView.layoutMarginsGuide
298 |
299 | view.translatesAutoresizingMaskIntoConstraints = false
300 |
301 | portraitConstraints.top = view.topAnchor.constraint(
302 | equalTo: parentView.bottomAnchor,
303 | constant: -minPortraitHeight)
304 | portraitConstraints.trailing = view.trailingAnchor.constraint(
305 | equalTo: parentView.trailingAnchor)
306 | portraitConstraints.leading = view.leadingAnchor.constraint(
307 | equalTo: parentView.leadingAnchor)
308 | portraitConstraints.bottom = view.bottomAnchor.constraint(
309 | equalTo: parentView.bottomAnchor,
310 | constant: cornerRadius)
311 |
312 | landscapeConstraints.top = view.topAnchor.constraint(
313 | equalTo: margins.topAnchor,
314 | constant: landscapeFrame.origin.y)
315 | landscapeConstraints.trailing = view.trailingAnchor.constraint(
316 | lessThanOrEqualTo: margins.trailingAnchor)
317 | .withPriority(.defaultHigh)
318 | landscapeConstraints.bottom = view.bottomAnchor.constraint(
319 | lessThanOrEqualTo: margins.bottomAnchor)
320 | .withPriority(.defaultHigh)
321 | landscapeConstraints.leading = view.leadingAnchor.constraint(
322 | equalTo: margins.leadingAnchor,
323 | constant: landscapeFrame.origin.x)
324 | landscapeConstraints.height = view.heightAnchor.constraint(
325 | equalToConstant: landscapeFrame.height)
326 | .withPriority(.defaultLow)
327 | landscapeConstraints.width = view.widthAnchor.constraint(
328 | equalToConstant: landscapeFrame.width)
329 | .withPriority(.defaultLow)
330 |
331 | if isPortrait {
332 | portraitConstraints.activate()
333 | } else {
334 | landscapeConstraints.activate()
335 | }
336 | }
337 |
338 | // Configure the constraints of the subviews within the pull up container.
339 | private func configureSubviewConstraints() {
340 | view.addSubview(headerView)
341 | view.addSubview(contentView)
342 |
343 | headerView.translatesAutoresizingMaskIntoConstraints = false
344 | contentView.translatesAutoresizingMaskIntoConstraints = false
345 |
346 | headerView.topAnchor.constraint(
347 | equalTo: view.topAnchor)
348 | .isActive = true
349 | headerView.trailingAnchor.constraint(
350 | equalTo: view.trailingAnchor)
351 | .withPriority(.defaultHigh)
352 | .isActive = true
353 | headerView.leadingAnchor.constraint(
354 | equalTo: view.leadingAnchor)
355 | .isActive = true
356 | headerViewHeightConstraint = headerView.heightAnchor.constraint(
357 | equalToConstant: headerViewHeight)
358 | .withActivitionState(true)
359 |
360 | contentView.topAnchor.constraint(
361 | equalTo: headerView.bottomAnchor)
362 | .isActive = true
363 | contentView.trailingAnchor.constraint(
364 | equalTo: view.trailingAnchor)
365 | .withPriority(.defaultHigh)
366 | .isActive = true
367 | contentView.leadingAnchor.constraint(
368 | equalTo: view.leadingAnchor)
369 | .isActive = true
370 |
371 | contentViewBottomConstraint
372 | .isActive = true
373 | }
374 |
375 | // If the landscape frame property is changes, adjust the landscape contraints
376 | private func updateLandscapeConstraints() {
377 |
378 | landscapeConstraints.top?.constant = landscapeFrame.origin.y
379 | landscapeConstraints.leading?.constant = landscapeFrame.origin.x
380 | landscapeConstraints.height?.constant = landscapeFrame.height
381 | landscapeConstraints.width?.constant = landscapeFrame.width
382 |
383 | guard self.viewIfLoaded?.window != nil else {
384 | return
385 | }
386 |
387 | UIView.animate(withDuration: 0.1, animations: {
388 | self.parent?.view.layoutIfNeeded()
389 | })
390 | }
391 |
392 | private var initialScrollViewContentOffset = CGPoint.zero
393 |
394 | @objc private func handleScrollViewPan(sender: UIPanGestureRecognizer) {
395 | guard
396 | isPortrait,
397 | let scrollview = sender.view as? UIScrollView,
398 | let topConstraint = portraitConstraints.top
399 | else {
400 | return
401 | }
402 |
403 | let isExpanded =
404 | topConstraint.constant <= -maxPortraitHeight
405 |
406 | let yTranslation = sender.translation(in: scrollview).y
407 | let isScrollingDown = sender.velocity(in: scrollview).y > 0
408 |
409 | let shouldDragViewDown =
410 | isScrollingDown && scrollview.contentOffset.y <= 0
411 |
412 | let shouldDragViewUp = !isScrollingDown && !isExpanded
413 | let shouldDragView = shouldDragViewDown || shouldDragViewUp
414 |
415 | if shouldDragView {
416 | scrollview.bounces = false
417 | scrollview.setContentOffset(.zero, animated: false)
418 | }
419 |
420 | switch sender.state {
421 | case .began:
422 | initialScrollViewContentOffset = scrollview.contentOffset
423 | case .changed:
424 | guard shouldDragView else {
425 | break
426 | }
427 | setTopOffset(topConstraint.constant + yTranslation - initialScrollViewContentOffset.y)
428 |
429 | sender.setTranslation(initialScrollViewContentOffset, in: scrollview)
430 | case .ended:
431 | scrollview.bounces = true
432 | goToNearestStickyPoint(verticalVelocity: sender.velocity(in: view).y)
433 | default:
434 | break
435 | }
436 | }
437 |
438 | // Handle panning the pull up container up and down
439 | @objc private func handlePan(_ sender: UIPanGestureRecognizer) {
440 |
441 | // Return if the pull up container isn't in portraint
442 | guard
443 | isPortrait,
444 | let topConstraint = portraitConstraints.top
445 | else {
446 | return
447 | }
448 |
449 | // Get the y translation of the pan
450 | let yTranslation = sender.translation(in: view).y
451 |
452 | switch sender.state {
453 | case .changed:
454 | // Change the offset of the pull up container from the bottom of it's parent view
455 | setTopOffset(topConstraint.constant + yTranslation, allowBounce: true)
456 | //Reset the y translation to zero
457 | sender.setTranslation(.zero, in: view)
458 | case .ended:
459 | // If the gesture is finished, move the pull up container to it's nearest sticky point
460 | goToNearestStickyPoint(verticalVelocity: sender.velocity(in: view).y)
461 | default:
462 | break
463 | }
464 | }
465 |
466 | // Setting the height up of the pull up view
467 | private func setTopOffset(
468 | _ value: CGFloat,
469 | animationDuration: TimeInterval? = nil,
470 | allowBounce: Bool = false)
471 | {
472 |
473 | // How far past the maximum and minimum height extents the pull up container is allowed to be dragged. Once released it will "bounce" back to the nearest extent.
474 | let bounceOffset = allowBounce ? self.bounceOffset : 0
475 | let minValue = -maxPortraitHeight - bounceOffset
476 | let maxValue = -minPortraitHeight + bounceOffset
477 |
478 | // The value to set the top offset to
479 | let targetValue = max(min(value, maxValue), minValue)
480 |
481 | // Changing the top contraint of the pull up container to the target value
482 | portraitConstraints.top?.constant = targetValue
483 |
484 | // Animating the height change. One feature that would be nice to add would be being able to perform other animations alongside this height change.
485 | UIView.animate(
486 | withDuration: animationDuration ?? 0,
487 | animations: {
488 | self.parent?.view.layoutIfNeeded()
489 | }) { _ in
490 | self.delegate?.pullUpContainer(statusDidChangeTo: .portrait(height: -targetValue))
491 | }
492 | }
493 |
494 | //Moving to the nearest sticky point
495 | private func goToNearestStickyPoint(verticalVelocity: CGFloat) {
496 | guard
497 | isPortrait,
498 | let topConstraint = portraitConstraints.top
499 | else {
500 | return
501 | }
502 |
503 | let currentPosition = topConstraint.constant
504 | let expandedPosition = -maxPortraitHeight
505 | let contractedPosition = -minPortraitHeight
506 |
507 | // Finding the nearest stickpoint
508 | let targetPosition =
509 | abs(currentPosition - expandedPosition) < abs(currentPosition - contractedPosition) ? expandedPosition : contractedPosition
510 |
511 | // Dividing distance to cover b animation duration to get the velocity of height change
512 | let distanceToCover = currentPosition - targetPosition
513 | let animationDuration = max(
514 | 0.08,
515 | min(0.3, TimeInterval(abs(distanceToCover/verticalVelocity))))
516 |
517 | // Setting the height to the sticky point's value
518 | setTopOffset(targetPosition, animationDuration: animationDuration)
519 | }
520 | }
521 |
522 |
523 | extension PullUpContainer {
524 | // Useful class for grouping portrait and landscape constraints.
525 | private class ConstraintFamily {
526 | var top: NSLayoutConstraint?
527 | var trailing: NSLayoutConstraint?
528 | var bottom: NSLayoutConstraint?
529 | var leading: NSLayoutConstraint?
530 | var height: NSLayoutConstraint?
531 | var width: NSLayoutConstraint?
532 |
533 | func activate() {
534 | setActivationStatus(to: true)
535 | }
536 |
537 | func deactivate() {
538 | setActivationStatus(to: false)
539 | }
540 |
541 |
542 | // A function for activating and deactivating all constraints
543 | private func setActivationStatus(to newStatus: Bool) {
544 |
545 | let constraints = [
546 | top,
547 | trailing,
548 | bottom,
549 | leading,
550 | height,
551 | width
552 | ].compactMap { $0 }
553 |
554 | if newStatus {
555 | NSLayoutConstraint.activate(constraints)
556 | } else {
557 | NSLayoutConstraint.deactivate(constraints)
558 | }
559 | }
560 |
561 | // Deactivate constraints before deinitializing.
562 | deinit {
563 | deactivate()
564 | }
565 | }
566 | }
567 |
568 | extension UIViewController {
569 | // Convinience function for adding a pull up container to a parent view controller
570 | func addPullUpContainer(_ pullUpContainer: PullUpContainer) {
571 | pullUpContainer.add(toParent: self)
572 | }
573 |
574 | // Convinience functions for removing a pull up container from a parent view controller
575 | func removePullUpContainer(_ pullUpContainer: PullUpContainer) {
576 | pullUpContainer.remove(fromParent: self, animated: true)
577 | }
578 | }
579 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/SelectDemoTableViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectDemoTableViewModel.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol SelectDemoTableViewModelDelegate: class {
12 | func selecDemoTableViewModel(didSelect demo: Demo)
13 | }
14 |
15 | // A view model for a simple demo selection tableView
16 | class SelectDemoTableViewModel: NSObject, TableViewModel {
17 | // An array of demo objects
18 | let demos = [
19 | Demo.makeHotelQuery(),
20 | Demo.makeChicagoBuildingsQuery(),
21 | Demo.makeChicagoTourismQuery(),
22 | Demo.makeBARTStopsQuery(),
23 | Demo.theatresNearBARTStopsQuery()
24 | ]
25 |
26 | // Initializing a simple selectable cellViewModel for each demo. The title is the corresponding demo's title.
27 | private lazy var cellViewModels: [SelectableCellViewModel] = demos.map { demo in
28 | let cellViewModel = SelectableCellViewModel(title: demo.title)
29 |
30 | // The selection handler of the cellViewModel notifies the delegate that the corresponding demo was selected.
31 | cellViewModel.selectionHandler = { [weak self] in
32 | self?.delegate?.selecDemoTableViewModel(didSelect: demo)
33 | }
34 | return cellViewModel
35 | }
36 |
37 | // An array holding all relavant cellViewModel types. Cell representable is the protocol cellViewModels must conform to.
38 | let cellViewModelTyes: [CellRepresentable.Type] = [
39 | SelectableCellViewModel.self
40 | ]
41 |
42 | // The handler that gets called when the bound tableView needs to be reloaded
43 | var reloadData: (() -> Void)?
44 |
45 | weak var delegate: SelectDemoTableViewModelDelegate?
46 |
47 | // Register all relevant table view cell types using the static function in each cellViewModel type.
48 | func registerCells(tableView: UITableView) {
49 | for cellViewModelType in cellViewModelTyes {
50 | cellViewModelType.registerCell(tableView: tableView)
51 | }
52 | }
53 |
54 | func numberOfRows(inSection section: Int) -> Int {
55 | cellViewModels.count
56 | }
57 |
58 | func getCellViewModel(at indexPath: IndexPath) -> CellRepresentable {
59 | let row = indexPath.row
60 | return cellViewModels[row]
61 | }
62 |
63 | // When a cell is selected, call the corresponding cellViewModel's selection handler.
64 | func handleCellSelection(at indexPath: IndexPath) {
65 | let row = indexPath.row
66 | cellViewModels[row].handleCellSelection()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Controllers/TableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewController.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // A basic MVVM TableViewController
12 | class TableViewController: UIViewController {
13 |
14 | let viewModel: TableViewModel
15 |
16 | private lazy var tableView: UITableView = {
17 | let tableView = UITableView()
18 | tableView.delegate = self
19 | tableView.dataSource = self
20 | return tableView
21 | }()
22 |
23 | init(viewModel: TableViewModel) {
24 | self.viewModel = viewModel
25 | super.init(nibName: nil, bundle: nil)
26 |
27 | viewModel.reloadData = { [weak self] in
28 | self?.tableView.reloadData()
29 | }
30 | }
31 |
32 | required init?(coder: NSCoder) {
33 | fatalError("init(coder:) has not been implemented")
34 | }
35 |
36 | override func viewDidLoad() {
37 | super.viewDidLoad()
38 | configure()
39 | viewModel.registerCells(tableView: tableView)
40 | }
41 |
42 | // Configure the tableView's constaints
43 | private func configure() {
44 | view.addSubview(tableView)
45 | tableView.translatesAutoresizingMaskIntoConstraints = false
46 | tableView.topAnchor.constraint(
47 | equalTo: view.topAnchor)
48 | .isActive = true
49 | tableView.trailingAnchor.constraint(
50 | equalTo: view.trailingAnchor)
51 | .withPriority(.defaultHigh)
52 | .isActive = true
53 | tableView.bottomAnchor.constraint(
54 | equalTo: view.bottomAnchor)
55 | .withPriority(.defaultHigh)
56 | .isActive = true
57 | tableView.leadingAnchor.constraint(
58 | equalTo: view.leadingAnchor)
59 | .isActive = true
60 | }
61 | }
62 |
63 | extension TableViewController: UITableViewDataSource {
64 | func numberOfSections(in tableView: UITableView) -> Int {
65 | return viewModel.numberOfSections
66 | }
67 |
68 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
69 | return viewModel.numberOfRows(inSection: section)
70 | }
71 |
72 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
73 | let cellViewModel = viewModel.getCellViewModel(at: indexPath)
74 | let cell = cellViewModel.cellInstance(tableView: tableView, indexPath: indexPath)
75 | return cell
76 | }
77 | }
78 |
79 | extension TableViewController: UITableViewDelegate {
80 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
81 | tableView.deselectRow(at: indexPath, animated: true)
82 | viewModel.handleCellSelection(at: indexPath)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "size" : "1024x1024",
46 | "scale" : "1x"
47 | }
48 | ],
49 | "info" : {
50 | "version" : 1,
51 | "author" : "xcode"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Models/Demo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Demo.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 | import SwiftOverpassAPI
11 |
12 | // A struct for storing information required to define an Overpass API demo.
13 | struct Demo {
14 | let title: String
15 | let resultUnit: String // Generic name for a query's result
16 | let defaultRegion: MKCoordinateRegion // Default query region
17 |
18 | // Takes a region and returns a query
19 | private let queryGenerator: (MKCoordinateRegion) -> String
20 |
21 | // Runs the query generation handler
22 | func generateQuery(forRegion region: MKCoordinateRegion) -> String {
23 | return queryGenerator(region)
24 | }
25 | }
26 |
27 | // Convinience functions for creating demo instances
28 | extension Demo {
29 |
30 | static func makeHotelQuery() -> Demo {
31 |
32 | let title = "St. Louis hotels"
33 | let resultUnit = "Hotel"
34 |
35 | let stLouisCoordinate = CLLocationCoordinate2D(
36 | latitude: 38.6270,
37 | longitude: -90.1994)
38 |
39 | let region = MKCoordinateRegion(
40 | center: stLouisCoordinate,
41 | latitudinalMeters: 10000,
42 | longitudinalMeters: 10000)
43 |
44 | let queryGenerator: (MKCoordinateRegion) -> String = { region in
45 |
46 | let boundingBox = OPBoundingBox.init(region: region)
47 |
48 | return try! OPQueryBuilder()
49 | .setElementTypes([.node, .way, .relation])
50 | .addTagFilter(key: "tourism", value: "hotel")
51 | .setBoundingBox(boundingBox)
52 | .setOutputType(.center)
53 | .buildQueryString()
54 | }
55 |
56 | return Demo(
57 | title: title,
58 | resultUnit: resultUnit,
59 | defaultRegion: region,
60 | queryGenerator: queryGenerator)
61 | }
62 |
63 | static func makeMultipolygonQuery() -> Demo {
64 |
65 | let title = "St. Louis multipolygons"
66 | let resultUnit = "Multipolygon"
67 |
68 | let stLouisCoordinate = CLLocationCoordinate2D(
69 | latitude: 38.6270,
70 | longitude: -90.1994)
71 |
72 | let region = MKCoordinateRegion(
73 | center: stLouisCoordinate,
74 | latitudinalMeters: 10000,
75 | longitudinalMeters: 10000)
76 |
77 | let queryGenerator: (MKCoordinateRegion) -> String = { region in
78 |
79 | let boundingBox = OPBoundingBox.init(region: region)
80 |
81 | return try! OPQueryBuilder()
82 | .setElementTypes([.relation])
83 | .addTagFilter(key: "type", value: "multipolygon")
84 | .setBoundingBox(boundingBox)
85 | .setOutputType(.recurseDown)
86 | .buildQueryString()
87 | }
88 |
89 | return Demo(
90 | title: title,
91 | resultUnit: resultUnit,
92 | defaultRegion: region,
93 | queryGenerator: queryGenerator)
94 | }
95 |
96 | static func makeChicagoBuildingsQuery() -> Demo {
97 |
98 | let title = "Chicago buildings"
99 | let resultUnit = "Building"
100 |
101 | let chicagoCoordinate = CLLocationCoordinate2D(
102 | latitude: 41.8781,
103 | longitude: -87.6298)
104 |
105 | let region = MKCoordinateRegion(
106 | center: chicagoCoordinate,
107 | latitudinalMeters: 5000,
108 | longitudinalMeters: 5000)
109 |
110 | let queryGenerator: (MKCoordinateRegion) -> String = { region in
111 |
112 | let boundingBox = OPBoundingBox(region: region)
113 |
114 | return try! OPQueryBuilder()
115 | .setTimeOut(180)
116 | .setElementTypes([.way, .relation])
117 | .addTagFilter(key: "building")
118 | .addTagFilter(key: "name")
119 | .setBoundingBox(boundingBox)
120 | .setOutputType(.geometry)
121 | .buildQueryString()
122 | }
123 |
124 | return Demo(
125 | title: title,
126 | resultUnit: resultUnit,
127 | defaultRegion: region,
128 | queryGenerator: queryGenerator)
129 | }
130 |
131 | static func makeChicagoTourismQuery() -> Demo {
132 |
133 | let title = "Chicago tourist attractions"
134 | let resultUnit = "Attraction"
135 |
136 | let chicagoCoordinate = CLLocationCoordinate2D(
137 | latitude: 41.8781,
138 | longitude: -87.6298)
139 |
140 | let region = MKCoordinateRegion(
141 | center: chicagoCoordinate,
142 | latitudinalMeters: 10000,
143 | longitudinalMeters: 10000)
144 |
145 | let queryGenerator: (MKCoordinateRegion) -> String = { region in
146 |
147 | let boundingBox = OPBoundingBox(region: region)
148 |
149 | return try! OPQueryBuilder()
150 | .setTimeOut(180)
151 | .setElementTypes([.node, .way, .relation])
152 | .addTagFilter(key: "tourism")
153 | .setBoundingBox(boundingBox)
154 | .setOutputType(.center)
155 | .buildQueryString()
156 | }
157 |
158 | return Demo(
159 | title: title,
160 | resultUnit: resultUnit,
161 | defaultRegion: region,
162 | queryGenerator: queryGenerator)
163 | }
164 |
165 | static func makeBARTStopsQuery() -> Demo {
166 |
167 | let title = "BART subway lines"
168 | let resultUnit = "Route"
169 |
170 | let sanFranciscoCoordinate = CLLocationCoordinate2D(
171 | latitude: 37.8044,
172 | longitude: -122.2712)
173 |
174 | let region = MKCoordinateRegion(
175 | center: sanFranciscoCoordinate,
176 | latitudinalMeters: 100000,
177 | longitudinalMeters: 100000)
178 |
179 | let queryGenerator: (MKCoordinateRegion) -> String = { region in
180 |
181 | let boundingBox = OPBoundingBox(region: region)
182 |
183 | return try! OPQueryBuilder()
184 | .setTimeOut(180)
185 | .setElementTypes([.relation])
186 | .addTagFilter(key: "network", value: "BART")
187 | .addTagFilter(key: "type", value: "route")
188 | .setBoundingBox(boundingBox)
189 | .setOutputType(.geometry)
190 | .buildQueryString()
191 | }
192 |
193 | return Demo(
194 | title: title,
195 | resultUnit: resultUnit,
196 | defaultRegion: region,
197 | queryGenerator: queryGenerator)
198 | }
199 |
200 | static func theatresNearBARTStopsQuery() -> Demo {
201 |
202 | let title = "Theaters near BART stops"
203 | let resultUnit = "Theater"
204 |
205 | let sanFranciscoCoordinate = CLLocationCoordinate2D(
206 | latitude: 37.7749,
207 | longitude: -122.4194)
208 |
209 | let region = MKCoordinateRegion(
210 | center: sanFranciscoCoordinate,
211 | latitudinalMeters: 100000,
212 | longitudinalMeters: 100000)
213 |
214 | let queryGenerator: (MKCoordinateRegion) -> String = { region in
215 |
216 | let boundingBoxString = OPBoundingBox(region: region).toString()
217 |
218 | let query = """
219 | data=[out:json];
220 | node["network"="BART"]
221 | ["railway"="stop"]
222 | \(boundingBoxString)
223 | ->.bartStops;
224 | (
225 | way(around.bartStops:200)["amenity"="cinema"];
226 | node(around.bartStops:200)["amenity"="cinema"];
227 | );
228 | out center;
229 | """
230 |
231 | return query
232 | }
233 |
234 | return Demo(
235 | title: title,
236 | resultUnit: resultUnit,
237 | defaultRegion: region,
238 | queryGenerator: queryGenerator)
239 | }
240 | }
241 |
242 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Navigation/OverpassDemoCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OverpassDemoCoordinator.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/11/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftOverpassAPI
11 |
12 | // A basic coordinatator class that controls navigation and view controller instantiation
13 | class OverpassDemoCoordinator {
14 |
15 | let navigationController: UINavigationController
16 |
17 | init(navigationController: UINavigationController) {
18 | self.navigationController = navigationController
19 | }
20 |
21 | // Starting the coordinator
22 | func start() {
23 | showDemoSelect(animated: false)
24 | }
25 |
26 | // On start show the demo select table view.
27 | private func showDemoSelect(animated: Bool) {
28 | let viewModel = SelectDemoTableViewModel()
29 | viewModel.delegate = self
30 | let viewController = TableViewController(viewModel: viewModel)
31 | viewController.title = "Select a demo"
32 | navigationController.pushViewController(viewController, animated: animated)
33 | }
34 | }
35 |
36 | extension OverpassDemoCoordinator: SelectDemoTableViewModelDelegate {
37 | // Whenever a demo is selected, navigate to that demo
38 | func selecDemoTableViewModel(didSelect demo: Demo) {
39 | let client = OPClient()
40 | client.endpoint = .kumiSystems
41 | let viewModel = DemoViewModel(demo: demo, overpassClient: client)
42 | let viewController = DemoViewController(viewModel: viewModel)
43 | viewController.title = demo.title
44 | navigationController.pushViewController(viewController, animated: true)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Protocols/CellRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CellRepresentable.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // Protocol for a cell view model that can register that cell's class and instantiate it's own cell
12 | protocol CellRepresentable {
13 | static func registerCell(tableView: UITableView)
14 | func cellInstance(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Protocols/CellSelectable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CellSelectable.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Protocol for a cell view model that can handle cell selection
12 | protocol CellSelectable {
13 | func handleCellSelection()
14 | }
15 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Protocols/MapViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapViewModel.swift
3 | // OSwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/11/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | // An API that provides information required to define a basic mapView's setup/behavior
12 | protocol MapViewModel: NSObject {
13 |
14 | var region: MKCoordinateRegion? { get }
15 | var annotations: [MKAnnotation] { get }
16 | var overlays: [MKOverlay] { get }
17 |
18 | var setRegion: ((MKCoordinateRegion) -> Void)? { get set }
19 | var addAnnotations: (([MKAnnotation]) -> Void)? { get set }
20 | var addOverlays: (([MKOverlay]) -> Void)? { get set }
21 | var removeAnnotations: (([MKAnnotation]) -> Void)? { get set }
22 | var removeOverlays: (([MKOverlay]) -> Void)? { get set }
23 |
24 | func registerAnnotationViews(to mapView: MKMapView)
25 | func renderer(for overlay: MKOverlay) -> MKOverlayRenderer
26 | func view(for annotation: MKAnnotation) -> MKAnnotationView?
27 | func userDidGestureOnMapView(sender: UIGestureRecognizer)
28 | }
29 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Protocols/TableViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewModel.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // An API that provides information required to define a basic tableView's setup/behavior
12 | protocol TableViewModel: NSObject {
13 | var numberOfSections: Int { get}
14 | var reloadData: (() -> Void)? { get set }
15 |
16 | func registerCells(tableView: UITableView)
17 | func numberOfRows(inSection section: Int) -> Int
18 | func getCellViewModel(at indexPath: IndexPath) -> CellRepresentable
19 | func handleCellSelection(at indexPath: IndexPath)
20 | }
21 |
22 | extension TableViewModel {
23 | var numberOfSections: Int {
24 | return 1
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Utilities/MKCoordinateRegion+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MKCoordinateRegion+Extensions.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/17/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | extension MKCoordinateRegion {
12 |
13 | /*
14 | Converting a map regin to a map rect.
15 | Copy-Pasta'd from:
16 | https://stackoverflow.com/questions/9270268/convert-mkcoordinateregion-to-mkmaprect
17 | */
18 |
19 | func toMKMapRect() -> MKMapRect {
20 | let topLeft = CLLocationCoordinate2D(
21 | latitude: self.center.latitude + (self.span.latitudeDelta/2.0),
22 | longitude: self.center.longitude - (self.span.longitudeDelta/2.0)
23 | )
24 |
25 | let bottomRight = CLLocationCoordinate2D(
26 | latitude: self.center.latitude - (self.span.latitudeDelta/2.0),
27 | longitude: self.center.longitude + (self.span.longitudeDelta/2.0)
28 | )
29 |
30 | let topLeftMapPoint = MKMapPoint(topLeft)
31 | let bottomRightMapPoint = MKMapPoint(bottomRight)
32 |
33 | let origin = MKMapPoint(
34 | x: topLeftMapPoint.x,
35 | y: topLeftMapPoint.y)
36 |
37 | let size = MKMapSize(
38 | width: fabs(bottomRightMapPoint.x - topLeftMapPoint.x),
39 | height: fabs(bottomRightMapPoint.y - topLeftMapPoint.y))
40 |
41 | return MKMapRect(origin: origin, size: size)
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Utilities/NSLayoutContstraint+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSLayoutContstraint+Extensions.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension NSLayoutConstraint {
12 |
13 | // Setting constraint priority using dot notation
14 | func withPriority(_ priority: UILayoutPriority ) -> NSLayoutConstraint {
15 | self.priority = priority
16 | return self
17 | }
18 |
19 | // Setting constraint activity using dot notation
20 | func withActivitionState(_ isActive: Bool) -> NSLayoutConstraint {
21 | self.isActive = isActive
22 | return self
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Utilities/UIColor+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Extensions.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/22/19.
6 | // Copyright © 2019 CocoaPods. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // Custom UIColor that can be changed to modify the color of all mapView overlays and annotations
12 | extension UIColor {
13 | static let theme = UIColor.blue
14 | }
15 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Utilities/UIView+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Extensions.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/18/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView {
12 |
13 | /*
14 | Creates an inner shadow on a UIView. Taken from:
15 | https://stackoverflow.com/questions/37668965/adding-inner-shadow-to-top-of-uiview
16 | */
17 | func addInnerShadow(
18 | to edges: [UIRectEdge],
19 | radius: CGFloat = 3.0,
20 | opacity: Float = 0.6,
21 | color: CGColor = UIColor.black.cgColor) {
22 |
23 | let fromColor = color
24 | let toColor = UIColor.clear.cgColor
25 | let viewFrame = self.frame
26 | for edge in edges {
27 | let gradientLayer = CAGradientLayer()
28 | gradientLayer.colors = [fromColor, toColor]
29 | gradientLayer.opacity = opacity
30 |
31 | switch edge {
32 | case .top:
33 | gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
34 | gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
35 | gradientLayer.frame = CGRect(x: 0.0, y: 0.0, width: viewFrame.width, height: radius)
36 | case .bottom:
37 | gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
38 | gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
39 | gradientLayer.frame = CGRect(x: 0.0, y: viewFrame.height - radius, width: viewFrame.width, height: radius)
40 | case .left:
41 | gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
42 | gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
43 | gradientLayer.frame = CGRect(x: 0.0, y: 0.0, width: radius, height: viewFrame.height)
44 | case .right:
45 | gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.5)
46 | gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.5)
47 | gradientLayer.frame = CGRect(x: viewFrame.width - radius, y: 0.0, width: radius, height: viewFrame.height)
48 | default:
49 | break
50 | }
51 | self.layer.addSublayer(gradientLayer)
52 | }
53 | }
54 | }
55 |
56 |
57 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Utilities/UIViewController+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+Extensions.swift
3 | // SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // Functions for adding and removing child view controllers from a parent view controller
12 | extension UIViewController {
13 |
14 | private func add(childViewController viewController: UIViewController) {
15 | addChild(viewController)
16 | viewController.didMove(toParent: self)
17 | }
18 |
19 | private func remove(asChildViewController viewController: UIViewController) {
20 | viewController.willMove(toParent: nil)
21 | viewController.view.removeFromSuperview()
22 | viewController.removeFromParent()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Views/DemoSelectCellViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoSelectCellViewModel.swift
3 | // OverpassDemo
4 | //
5 | // Created by Edward Samson on 10/8/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class DemoSelectCellViewModel {
12 |
13 | private static let reuseIdentifier = "DemoSelectCell"
14 |
15 | let demo: Demo
16 | var selectionHandler: (() -> Void)?
17 |
18 | init(demo: Demo) {
19 | self.demo = demo
20 | }
21 | }
22 |
23 | extension DemoSelectCellViewModel: CellRepresentable {
24 | static func registerCell(tableView: UITableView) {
25 |
26 | tableView.register(UITableViewCell.self,forCellReuseIdentifier: reuseIdentifier)
27 | }
28 |
29 | func cellInstance(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
30 | let cell = tableView.dequeueReusableCell(withIdentifier: DemoSelectCellViewModel.reuseIdentifier, for: indexPath)
31 | cell.textLabel?.text = demo.title
32 | return cell
33 | }
34 | }
35 |
36 | extension DemoSelectCellViewModel: CellSelectable {
37 | func handleCellSelection() {
38 | selectionHandler?()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Views/PullUpHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HandleView.swift
3 | // OverpassDemo
4 | //
5 | // Created by Edward Samson on 10/17/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class PullUpHeaderView: UIView {
12 |
13 | lazy var handleView: UIView = {
14 |
15 | let handleWidth: CGFloat = 50
16 | let handleHeight: CGFloat = 8
17 | let cornerRadius: CGFloat = handleHeight / 2
18 | let originX: CGFloat = bounds.midX - handleWidth / 2
19 | let originY: CGFloat = 8
20 |
21 | let handleRect = CGRect(
22 | x: originX,
23 | y: originY,
24 | width: handleWidth,
25 | height: handleHeight)
26 |
27 | let view = UIView(frame: handleRect)
28 | view.backgroundColor = UIColor(white: 0.93, alpha: 1.0)
29 | view.layer.borderColor = UIColor(white: 0.8, alpha: 1.0).cgColor
30 | view.layer.borderWidth = 1
31 | view.addInnerShadow(to: [.top, .right], radius: 4, opacity: 0.6)
32 | view.clipsToBounds = true
33 |
34 | return view
35 | }()
36 |
37 | init() {
38 | super.init(frame: CGRect.zero)
39 | configure()
40 | }
41 |
42 | required init?(coder: NSCoder) {
43 | fatalError("init(coder:) has not been implemented")
44 | }
45 |
46 | override func layoutSubviews() {
47 | super.layoutSubviews()
48 | handleView.layer.cornerRadius = handleView.bounds.height / 2
49 | }
50 |
51 | private func configure() {
52 | addSubview(handleView)
53 | handleView.translatesAutoresizingMaskIntoConstraints = false
54 |
55 | handleView.centerXAnchor.constraint(
56 | equalTo: centerXAnchor)
57 | .isActive = true
58 | handleView.topAnchor.constraint(
59 | equalTo: topAnchor,
60 | constant: 8)
61 | .isActive = true
62 | handleView.bottomAnchor.constraint(
63 | lessThanOrEqualTo: bottomAnchor,
64 | constant: -8)
65 | .withPriority(.defaultHigh)
66 | .isActive = true
67 | handleView.heightAnchor.constraint(equalToConstant: 8)
68 | .isActive = true
69 | handleView.widthAnchor.constraint(equalToConstant: 50)
70 | .isActive = true
71 | }
72 | }
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Example/SwiftOverpassAPI/Views/SelectableCellViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectableCellViewModel.swift
3 | // OverpassDemo
4 | //
5 | // Created by Edward Samson on 10/15/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SelectableCellViewModel {
12 |
13 | private static let reuseIdentifier = "SelectableCellViewModel"
14 |
15 | let title: String
16 | var selectionHandler: (() -> Void)?
17 |
18 | init(title: String) {
19 | self.title = title
20 | }
21 | }
22 |
23 | extension SelectableCellViewModel: CellRepresentable {
24 | static func registerCell(tableView: UITableView) {
25 |
26 | tableView.register(
27 | UITableViewCell.self,
28 | forCellReuseIdentifier: reuseIdentifier)
29 | }
30 |
31 | func cellInstance(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
32 |
33 | let cell = tableView.dequeueReusableCell(
34 | withIdentifier: SelectableCellViewModel.reuseIdentifier,
35 | for: indexPath)
36 |
37 | cell.textLabel?.text = title
38 | return cell
39 | }
40 | }
41 |
42 | extension SelectableCellViewModel: CellSelectable {
43 | func handleCellSelection() {
44 | selectionHandler?()
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/Example/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Example/Tests/Tests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import SwiftOverpassAPI
3 |
4 | class Tests: XCTestCase {
5 |
6 | override func setUp() {
7 | super.setUp()
8 | // Put setup code here. This method is called before the invocation of each test method in the class.
9 | }
10 |
11 | override func tearDown() {
12 | // Put teardown code here. This method is called after the invocation of each test method in the class.
13 | super.tearDown()
14 | }
15 |
16 | func testExample() {
17 | // This is an example of a functional test case.
18 | XCTAssert(true, "Pass")
19 | }
20 |
21 | func testPerformanceExample() {
22 | // This is an example of a performance test case.
23 | self.measure() {
24 | // Put the code you want to measure the time of here.
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 ebsamson3
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftOverpassAPI
2 |
3 | [](https://travis-ci.org/ebsamson3/SwiftOverpassAPI)
4 | [](https://cocoapods.org/pods/SwiftOverpassAPI)
5 | [](https://cocoapods.org/pods/SwiftOverpassAPI)
6 | [](https://cocoapods.org/pods/SwiftOverpassAPI)
7 |
8 |
9 |
10 |
11 |
12 | A Swift module for querying, decoding, and visualizing Overpass API data.
13 |
14 | ### **What is Overpass API?**
15 |
16 | Overpass API is a read only database for querying open source mapping information provided by the OpenStreetMap project. For more information visit the [Overpass API Wiki](https://wiki.openstreetmap.org/wiki/Overpass_API) and the [OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/Main_Page).
17 |
18 | ## **Installation**
19 |
20 | SwiftOverpassAPI is available through [CocoaPods](https://cocoapods.org). To install
21 | it, simply add the following line to your Podfile:
22 |
23 | ```ruby
24 | pod 'SwiftOverpassAPI'
25 | ```
26 |
27 | ## **Usage**
28 |
29 | ### **Creating a bounding box**
30 |
31 | Create a boxed region that will confine your query:
32 |
33 | **Option 1:** Initialize with a MKCoordinateRegion:
34 | ```swift
35 | let center = CLLocationCoordinate2D(
36 | latitude: 37.7749,
37 | longitude: -122.4194)
38 |
39 | let queryRegion = MKCoordinateRegion(
40 | center: center,
41 | latitudinalMeters: 50000,
42 | longitudinalMeters: 50000)
43 |
44 | let boundingBox = OPBoundingBox(region: region)
45 | ```
46 |
47 | **Option 2:** Initialize with latitudes and longitudes:
48 | ```swift
49 | let boundingBox = OPBoundingBox(
50 | minLatitude: 38.62661651293796,
51 | minLongitude: -90.1998908782745,
52 | maxLatitude: 38.627383487062005,
53 | maxLongitude: -90.1989091217254)
54 | ```
55 |
56 | ### **Building a Query**
57 |
58 | For simple query generation, you can use `OPQueryBuilder` class:
59 |
60 | ```swift
61 | do {
62 | let query = try OPQueryBuilder()
63 | .setTimeOut(180) //1
64 | .setElementTypes([.relation]) //2
65 | .addTagFilter(key: "network", value: "BART", exactMatch: false) //3
66 | .addTagFilter(key: "type", value: "route") //4
67 | .addTagFilter(key: "name") //5
68 | .setBoundingBox(boundingBox) //6
69 | .setOutputType(.geometry) //7
70 | .buildQueryString() //8
71 | } catch {
72 | print(error.localizedDescription)
73 | }
74 | ```
75 |
76 | 1) Set a timeout for the server request
77 | 2) Set one or more element types that you wish to query (Any combination of `.node`, `.way` and/or `.relation`)
78 | 3) Filter for elements whose "network" tag's value contains "BART" (case insensitive)
79 | 4) Filter for elements whose "type" tag's value is exactly "route"
80 | 5) Filter for all elements with a "name" tag. Can have any associated value.
81 | 6) Query within the specified bounding box
82 | 7) Specify the output type of the query (See "Choosing a query output type" below)
83 | 8) Build a query string that you pass to the overpass client that makes requests to an Overpass API endpoint
84 |
85 | 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:
86 |
87 | ```swift
88 | let boundingBoxString = OPBoundingBox(region: region).toString()
89 |
90 | let query = """
91 | data=[out:json];
92 | node["network"="BART"]
93 | ["railway"="stop"]
94 | \(boundingBoxString)
95 | ->.bartStops;
96 | (
97 | way(around.bartStops:200)["amenity"="cinema"];
98 | node(around.bartStops:200)["amenity"="cinema"];
99 | );
100 | out center;
101 | """
102 | ```
103 |
104 | This query finds all theaters less than 200 meters from any BART (Bay Area Rapid Transit) stop. 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/).
105 |
106 | ### **Choosing a query output type**
107 |
108 | When using `OPQueryBuiler` you can choose from the following output types:
109 |
110 | ```swift
111 | public enum OPQueryOutputType {
112 | case standard, center, geometry, recurseDown, recurseUp, recurseUpAndDown
113 |
114 | // The Overpass API language syntax for each output type
115 | func toString() -> String {
116 | switch self {
117 | case .standard:
118 | return "out;"
119 | case .recurseDown:
120 | return "(._;>;);out;"
121 | case .recurseUp:
122 | return "(._;<;);out;"
123 | case .recurseUpAndDown:
124 | return "((._;<;);>;);out;"
125 | case .geometry:
126 | return "out geom;"
127 | case .center:
128 | return "out center;"
129 | }
130 | }
131 | }
132 | ```
133 | - **Standard:** Basic output that does not fetch additional elements or geometry information
134 | - **Recurse Down:** Enables full geometry reconstruction of query elements. Returns the queried elements plus:
135 | - all nodes that are part of a way which appears in the initial result set; plus
136 | - all nodes and ways that are members of a relation which appears in the initial result set; plus
137 | - all nodes that are part of a way which appears in the initial result set
138 | - **Recurse Up:** Returns the queried elements plus:
139 | - all ways that have a node which appears in the initial result set
140 | - all relations that have a node or way which appears in the initial result set
141 | - all relations that have a way which appears in the result initial result set
142 | - **Recurse Up and Down:** Recurse up then recurse down on the results of the upwards recursion
143 | - **Geometry:** Returned elements full geometry information that is sufficient for visualization
144 | - **Center:** Returned elements contain their center coordinate. Best/most efficient option when you don't want to visualize full element geometries.
145 |
146 | ### **Making an Overpass request**
147 |
148 | ```swift
149 | let client = OPClient() //1
150 | client.endpoint = .kumiSystems //2
151 |
152 | //3
153 | client.fetchElements(query: query) { result in
154 | switch result {
155 | case .failure(let error):
156 | print(error.localizedDescription)
157 | case .success(let elements):
158 | print(elements) // Do something with returned the elements
159 | }
160 | }
161 | ```
162 |
163 | 1) Instantiate a client
164 | 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.
165 | 3) Fetch elements: The decoded response will be in the form of a dictionary of Overpass elements keyed by their database id.
166 |
167 | ### **Generating MapKit Visualizations**
168 |
169 | Generate visualizations for all elements the returned element dictionary:
170 |
171 | ```swift
172 | // Creates a dictionary of mapkit visualizations keyed by the corresponding element's id
173 | let visualizations = OPVisualizationGenerator
174 | .mapKitVisualizations(forElements: elements)
175 | ```
176 |
177 | Generate a visualization for an individual element:
178 |
179 | ```swift
180 | if let visualization = OPVisualizationGenerator.mapKitVisualization(forElement: element) {
181 | // Do something
182 | } else {
183 | print("Element doesn't have a geometry to visualize")
184 | }
185 | ```
186 |
187 | ### **Displaying Visualizations via MKMapView**
188 |
189 | **Step 1:** Add overlays and annotations to mapView using the included visualization generator
190 |
191 | ```swift
192 | func addVisualizations(_ visualizations: [Int: OPMapKitVisualization]) {
193 |
194 | var annotations = [MKAnnotation]()
195 | var polylines = [MKPolyline]()
196 | var polygons = [MKPolygon]()
197 |
198 | for visualization in visualizations.values {
199 | switch visualization {
200 | case .annotation(let annotation):
201 | newAnnotations.append(annotation)
202 | case .polyline(let polyline):
203 | polylines.append(polyline)
204 | case .polylines(let newPolylines):
205 | polylines.append(contentsOf: newPolylines)
206 | case .polygon(let polygon):
207 | polygons.append(polygon)
208 | case .polygons(let newPolygons):
209 | polygons.append(contentsOf: newPolygons)
210 | }
211 | }
212 |
213 | if #available(iOS 13, *) {
214 | // MKMultipolyline and MKMultipolygon generate a single renderer for all of their elements. If available, it is more efficient than creating a renderer for each overlay.
215 | let multiPolyline = MKMultiPolyline(polylines)
216 | let multiPolygon = MKMultiPolygon(polygons)
217 | mapView.addOverlay(multiPolygon)
218 | mapView.addOverlay(multiPolyline)
219 | } else {
220 | mapView.addOverlays(polygons)
221 | mapView.addOverlays(polylines)
222 | }
223 |
224 | mapView.addAnnotations(annotations)
225 | }
226 | ```
227 |
228 | Depending on its case, a visualization can have one of the following associated values types:
229 | 1) `MKAnnotation`: For single coordinates. The title of the annotation is the value of the element's name tag.
230 | 2) `MKPolyline`: Commonly used for roads
231 | 3) `MKPolygon`: Commonly used for simple structures like buildings
232 | 4) `[MKPolyline]`: An array of related polylines in a collection such as a route or a waterway
233 | 5) `[MKPolygon]`: An array of related polygons that make up a more complicated structures.
234 |
235 | **Step 2:** Display views for the overlays and annotations
236 |
237 | ```swift
238 | extension MapViewController: MKMapViewDelegate {
239 | // Delegate method for rendering overlays
240 | func mapView(
241 | _ mapView: MKMapView,
242 | rendererFor overlay: MKOverlay) -> MKOverlayRenderer
243 | {
244 | let strokeWidth: CGFloat = 2
245 | let strokeColor = UIColor.theme
246 | let fillColor = UIColor.theme.withAlphaComponent(0.5)
247 |
248 | if let polyline = overlay as? MKPolyline {
249 | let renderer = MKPolylineRenderer(
250 | polyline: polyline)
251 | renderer.strokeColor = strokeColor
252 | renderer.lineWidth = strokeWidth
253 | return renderer
254 | } else if let polygon = overlay as? MKPolygon {
255 | let renderer = MKPolygonRenderer(
256 | polygon: polygon)
257 | renderer.fillColor = fillColor
258 | renderer.strokeColor = strokeColor
259 | renderer.lineWidth = strokeWidth
260 | return renderer
261 | } else if let multiPolyline = overlay as? MKMultiPolyline {
262 | let renderer = MKMultiPolylineRenderer(
263 | multiPolyline: multiPolyline)
264 | renderer.strokeColor = strokeColor
265 | renderer.lineWidth = strokeWidth
266 | return renderer
267 | } else if let multiPolygon = overlay as? MKMultiPolygon {
268 | let renderer = MKMultiPolygonRenderer(
269 | multiPolygon: multiPolygon)
270 | renderer.fillColor = fillColor
271 | renderer.strokeColor = strokeColor
272 | renderer.lineWidth = strokeWidth
273 | return renderer
274 | } else {
275 | return MKOverlayRenderer()
276 | }
277 | }
278 |
279 | /*
280 | // Make sure to add the following when configure your mapView:
281 |
282 | let markerReuseIdentifier = "MarkerAnnotationView"
283 |
284 | mapView.register(
285 | MKMarkerAnnotationView.self,
286 | forAnnotationViewWithReuseIdentifier: markerReuseIdentifier)
287 | */
288 |
289 | // Delegate method for setting annotation views.
290 | func mapView(
291 | _ mapView: MKMapView,
292 | viewFor annotation: MKAnnotation) -> MKAnnotationView?
293 | {
294 | guard
295 | let pointAnnotation = annotation as? MKPointAnnotation
296 | else {
297 | return nil
298 | }
299 |
300 | let view = MKMarkerAnnotationView(
301 | annotation: pointAnnotation,
302 | reuseIdentifier: markerReuseIdentifier)
303 |
304 | view.markerTintColor = UIColor.theme
305 | return view
306 | }
307 | }
308 | ```
309 |
310 | ## **Example App**
311 |
312 |
313 |
314 |
315 |
316 |
317 | To run the example project, clone the repo, and run `pod install` from the Example directory first.
318 |
319 | ## **Author**
320 |
321 | ebsamson3, ebsamson3@gmail.com
322 |
323 | ## **Aknowledgements**
324 |
325 | Thanks to all those who contribute to Overpass API and OpenStreetMap. Thank you to [Martin Raifer](https://github.com/tyrasd), whose [osmtogeojson](https://github.com/tyrasd/osmtogeojson) code saved me a lot of time helped me understand out how to process Overpass API elements.
326 |
327 | ## **License**
328 |
329 | SwiftOverpassAPI is available under the MIT license. See the LICENSE file for more info.
330 |
--------------------------------------------------------------------------------
/Screenshots/bart_lines_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebsamson3/SwiftOverpassAPI/a96bea6ea42cd54197a9bf989b5ebcf6624403dc/Screenshots/bart_lines_screenshot.png
--------------------------------------------------------------------------------
/Screenshots/buildings_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebsamson3/SwiftOverpassAPI/a96bea6ea42cd54197a9bf989b5ebcf6624403dc/Screenshots/buildings_screenshot.png
--------------------------------------------------------------------------------
/Screenshots/busch_stadium_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebsamson3/SwiftOverpassAPI/a96bea6ea42cd54197a9bf989b5ebcf6624403dc/Screenshots/busch_stadium_screenshot.png
--------------------------------------------------------------------------------
/Screenshots/tourism_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebsamson3/SwiftOverpassAPI/a96bea6ea42cd54197a9bf989b5ebcf6624403dc/Screenshots/tourism_screenshot.png
--------------------------------------------------------------------------------
/Source/Controllers/OPQueryBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPQueryBuilder.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/11/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | /*:
10 | A convinience class for creating simple queries written in the Overpass API language.
11 | For more information, see the Overpass API Language Guide:
12 | https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide
13 | */
14 |
15 | import Foundation
16 |
17 | public class OPQueryBuilder {
18 |
19 | // Overpass API results have a dictionary of descriptive tag/value pairs. The query builder uses the tag filter struct to filter based on these values.
20 | struct TagFilter {
21 | let key: String
22 | let value: String?
23 | let exactMatch: Bool // True: Filter passes any value that matches the Tag Filter value property, False: Filters passes any value that contains the tag filter value property (case insensitive).
24 |
25 | func toString() -> String {
26 | //If the value property of the Tag Filter is nil, Filter for any result that contains the Tag Filters key, regardless of the key's corresponding value.
27 | guard let value = value else {
28 | return "[\(key)]"
29 | }
30 |
31 | // exactMatchOnly == true: Filter passes any value that matches the Tag Filter value property. exactMatchOnly == false: Filters passes any value that contains the tag filter value property (case insensitive).
32 | return exactMatch ?
33 | "[\"\(key)\"=\"\(value)\"]" :
34 | "[\"\(key)\"~\"\(value)\",i]"
35 | }
36 | }
37 |
38 | private var tagFilters = [TagFilter]()
39 | private var boundingBox: OPBoundingBox?
40 | private var elementTypes = Set()
41 | private var outputType: OPQueryOutputType = .standard
42 | private var timeOut: Int?
43 | private var maxSize: Int?
44 |
45 | public init() {}
46 |
47 | // Add a new filter that checks for the presence of a particular tag key or tag key/value pair in an Overpass element's descriptive data
48 | public func addTagFilter(
49 | key: String,
50 | value: String? = nil,
51 | exactMatch: Bool = true) -> Self
52 | {
53 | let tagFilter = TagFilter(
54 | key: key,
55 | value: value,
56 | exactMatch: exactMatch)
57 |
58 | tagFilters.append(tagFilter)
59 |
60 | return self
61 | }
62 |
63 | // Set search region for query. Defaults to a bounding box for the entire earch.
64 | public func setBoundingBox(_ boundingBox: OPBoundingBox) -> Self {
65 |
66 | self.boundingBox = boundingBox
67 |
68 | return self
69 | }
70 |
71 | // Set the element types the query can return. Possible types: Node, Way, and Relation
72 | public func setElementTypes(_ elementTypes: Set) -> Self {
73 | self.elementTypes = elementTypes
74 | return self
75 | }
76 |
77 | //Sets teh output type of a query. Can return center. See OverpassQueryOutputType.swift
78 | public func setOutputType(_ outputType: OPQueryOutputType) -> Self {
79 | self.outputType = outputType
80 | return self
81 | }
82 |
83 | // Not sure if this works. The query will still run but it doesn't appear as if the timeout length was effected. It may be due to the endpoint I have used to test this API.
84 | public func setTimeOut(_ timeOut: Int) -> Self {
85 | self.timeOut = timeOut
86 | return self
87 | }
88 |
89 | // Set max size of results in bytes
90 | public func setMaxSize(_ maxSize: Int) -> Self {
91 | self.maxSize = maxSize
92 | return self
93 | }
94 |
95 | // Generate a string representation of the query in the Overpass API language.
96 | public func buildQueryString() throws -> String {
97 |
98 | let elementTypeCount = elementTypes.count
99 |
100 | // Query will throw if you do not set a least one returned element type.
101 | guard elementTypeCount > 0 else {
102 | throw OPQueryBuilderError.noElementTypesSpecified
103 | }
104 |
105 | // Header specifying a JSON response from the Overpass endpoint
106 | let dataOutputString = "data=[out:json]"
107 |
108 | // If querying multiple element types, the types need to be grouped by parenthesis.
109 | let elementGroupStart = elementTypeCount > 1 ? "(" : ""
110 | let elementGroupEnd = elementTypeCount > 1 ? ");" : ""
111 |
112 | // Generate Overpass language strings for the query's tag filters, bounding box, and JSON data output structure.
113 | let tagFilterString = tagFilters.map { $0.toString() }.joined()
114 | let boundingBoxString = boundingBox?.toString() ?? ""
115 | let outputTypeString = outputType.toString()
116 |
117 | let timeOutString: String
118 |
119 | // Add a paramter for the timout of the query if it exists
120 | if let timeOut = timeOut {
121 | timeOutString = "[timeout:\(timeOut)]"
122 | } else {
123 | timeOutString = ""
124 | }
125 |
126 | let maxSizeString: String
127 |
128 | // Add a paramater for the max size to the query if it exists
129 | if let maxSize = maxSize {
130 | maxSizeString = "[maxsize:\(maxSize)]"
131 | } else {
132 | maxSizeString = ""
133 | }
134 |
135 | // Combined query header containing the optional time out and max size parameters
136 | let headerString = dataOutputString + timeOutString + maxSizeString + ";"
137 |
138 |
139 | // For each expected element type, add all tag filters and specify the square search region (bounding box). Then join the commands for each element type into a single string to form the main body of the query.
140 | let queryBody: String = elementTypes.map { elementType in
141 |
142 | let elementTypeString = elementType.shortString
143 |
144 | let substring = String(
145 | format: "%@%@%@%@",
146 | elementTypeString,
147 | tagFilterString,
148 | boundingBoxString,
149 | ";")
150 |
151 | return substring
152 |
153 | }.joined()
154 |
155 | // Combine the header, body, and output type query components to form the completed query.
156 | let queryString = String(
157 | format: "%@%@%@%@%@",
158 | headerString,
159 | elementGroupStart,
160 | queryBody,
161 | elementGroupEnd,
162 | outputTypeString)
163 |
164 | return queryString
165 | }
166 | }
167 |
168 |
169 |
--------------------------------------------------------------------------------
/Source/Controllers/OPVisualizationGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPVisualizationGenerator.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 |
10 | import MapKit
11 |
12 | public struct OPVisualizationGenerator {
13 |
14 | // Pass in an array of decoded overpass objects to get their respective mapkit visualizations (annotations and polygons, depending on object type).
15 |
16 | public static func mapKitVisualizations(
17 | forElements elements: [Int: OPElement]) -> [Int: OPMapKitVisualization]
18 | {
19 |
20 | // For each key: Overpass ID and value: Decodeded Overpass Element
21 |
22 | return Dictionary(uniqueKeysWithValues: elements.compactMap({ (id, element) in
23 |
24 | // Avoids generating visualizations for uninteresting overpass elements (for example, annonymous nodes that make up a way/relation that is already being generated.
25 | guard element.isInteresting else {
26 | return nil
27 | }
28 |
29 | // Avoid generating visualizations for elements that have already been rendered as a member of a parent element.
30 | guard !element.isSkippable else {
31 | return nil
32 | }
33 |
34 | // Generate mapkit visualization
35 | guard let annotation = mapKitVisualization(forElement: element) else {
36 | return nil
37 | }
38 |
39 | // Return visualization with same key as specified in input dictionary. Typically this is the ID for the overpass element being visualized.
40 | return (id, annotation)
41 | }))
42 | }
43 |
44 | // Generates a mapkit visualization for a given decoded overpass element
45 | public static func mapKitVisualization(forElement element: OPElement) -> OPMapKitVisualization? {
46 |
47 | // Different element geometries require different classes of mapkit visualizations
48 | switch element.geometry {
49 | case .none:
50 | return nil
51 | case .center(let coordinate):
52 | // Single coordinate geometries are rendered as annotations
53 | let annotation = MKPointAnnotation()
54 | annotation.coordinate = coordinate
55 | annotation.title = element.tags[Overpass.Keys.name]
56 | return .annotation(annotation)
57 | case .polyline(let coordinates):
58 | // Unclosed coordinate arrays are rendered as polylines
59 | let polyline = MKPolyline(
60 | coordinates: coordinates,
61 | count: coordinates.count)
62 | polyline.title = element.tags[Overpass.Keys.name]
63 | return .polyline(polyline)
64 | case .polygon(let coordinates):
65 | // Close coordinate arrays are rendered as polygons
66 | let polygon = MKPolygon(
67 | coordinates: coordinates,
68 | count: coordinates.count)
69 | polygon.title = element.tags[Overpass.Keys.name]
70 | return .polygon(polygon)
71 | case .multiPolyline(let coordinatesArray):
72 | // Multiple unclosed arrays of coordinates (for example, a collection of streets) are converted into an array of polylines
73 | let polylines = coordinatesArray.map {
74 | MKPolyline(coordinates: $0, count: $0.count)
75 | }
76 | return .polylines(polylines)
77 | case .multiPolygon(let nestedPolygonCoordinatesArray):
78 | //Multiple nested polygon coordinate arrays (typically buildings made up of multiple polygons) are converted into an array of polygons.
79 |
80 | // For each nested coordinate array (a single outer ring containing any number of inner rings). Rings being any closed array of coorinate.
81 | let polygons: [MKPolygon] = nestedPolygonCoordinatesArray.map {
82 |
83 | // Generate polygons for the inner rings
84 | let innerPolygons = $0.innerRings.map { coordinates in
85 | MKPolygon(
86 | coordinates: coordinates,
87 | count: coordinates.count)
88 | }
89 |
90 | // Create the outer polygon and set all the inner polygons in the nested coordinate array as interior polygons.
91 | return MKPolygon(
92 | coordinates: $0.outerRing,
93 | count: $0.outerRing.count,
94 | interiorPolygons: innerPolygons)
95 | }
96 |
97 | return .polygons(polygons)
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Source/Controllers/PolygonChecker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PolygonChecker.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/6/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | // Checks whether or not a way (an array of lat/lon pairs) returned by the overpass API forms a polygon or not
12 | struct PolygonChecker {
13 |
14 | // Keys that are valid for polygons EXCEPT when they have the following associated values
15 | private static let blacklist : [String: Set] = [
16 | "area:highway": ["no"],
17 | "aeroway": ["no", "taxiway"],
18 | "amenity": ["no"],
19 | "boundary": ["no"],
20 | "building:part": ["no"],
21 | "building": ["no"],
22 | "craft": ["no"],
23 | "golf": ["no"],
24 | "historic": ["no"],
25 | "indoor": ["no"],
26 | "landuse": ["no"],
27 | "leisure": ["no", "cutline", "embankment", "pipeline"],
28 | "natural": ["no", "coastline", "cliff", "ridge", "arete", "tree_row"],
29 | "office": ["no"],
30 | "place": ["no"],
31 | "public_transport": ["no"],
32 | "ruins": ["no"],
33 | "shop": ["no"],
34 | "tourism": ["no"]
35 | ]
36 |
37 | // Keys that are valid for polygons whenever the have the following associated values
38 | private static let whitelist : [String: Set] = [
39 | "barrier": ["city_wall", "ditch", "hedge", "retaining_wall", "wall, spikes"],
40 | "highway": ["services", "rest_area", "escape", "elevator"],
41 | "power": ["plant", "substation", "generator", "transformer"],
42 | "railway": ["station", "turntable", "roundhouse", "platform"],
43 | "waterway": ["riverbank", "dock", "boatyard", "dam"]
44 | ]
45 |
46 | // Checks to see whether the tags/geometry are valid for a polygon
47 | static func checkWay(
48 | withCoordinates coordinates: [CLLocationCoordinate2D],
49 | andTags tags: [String: String]) -> Bool
50 | {
51 | return check(coordinates: coordinates) && check(tags: tags)
52 | }
53 |
54 | // Check for tags that are always included/never included in polygons
55 | private static func check(tags: [String: String]) -> Bool {
56 |
57 | // If the way has an "area" tag that isn't set to "no"
58 | if let areaValue = tags[Overpass.Keys.area] {
59 | return areaValue == Overpass.Values.no ? false : true
60 | }
61 |
62 | for (key, value) in tags {
63 |
64 | // If the key is valid and the value isn't on the blacklist (See blacklist for more details)
65 | if
66 | let blacklistedValues = blacklist[key],
67 | !blacklistedValues.contains(value)
68 | {
69 | return true
70 | }
71 |
72 | // Checks to see if the tag matches any whitelisted key/value pairs (See whitelist for more details)
73 | if
74 | let whitelistedValues = whitelist[key],
75 | whitelistedValues.contains(value)
76 | {
77 | return true
78 | }
79 | }
80 |
81 | // If no tags specify that the way is a polygon, return false
82 | return false
83 | }
84 |
85 | //Check to make sure that there are at least 4 coordinates and that the first and last coordinates are the same. If both are true than the way geometry is valid for a polygon.
86 | private static func check(coordinates: [CLLocationCoordinate2D]) -> Bool {
87 | if
88 | coordinates.count > 3,
89 | let firstCoordinate = coordinates.first,
90 | let lastCoordinate = coordinates.last,
91 | firstCoordinate.isEqual(to: lastCoordinate)
92 | {
93 | return true
94 | }
95 |
96 | return false
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Source/Controllers/TagChecker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TagChecker.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Checks whether an element contains tags that aren't included in the uninteresting tags set
12 | struct TagChecker {
13 |
14 | static let uninterestingTags: Set = [
15 | "source",
16 | "source_ref",
17 | "source:ref",
18 | "history",
19 | "attribution",
20 | "created_by",
21 | "tiger:county",
22 | "tiger:tlid",
23 | "tiger:upload_uuid"
24 | ]
25 |
26 | static func checkForInterestingTags(amongstTags tags: [String: String]) -> Bool {
27 | for key in tags.keys {
28 | if !uninterestingTags.contains(key) {
29 | return true
30 | }
31 | }
32 | return false
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Source/Models/ElementCodingKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ElementCodingKeys.swift
3 | // OverpassApiVisualizer
4 | //
5 | // Created by Edward Samson on 10/13/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Coding keys that may be found w/in a single element of an overpass API result
12 | enum ElementCodingKeys: String, CodingKey {
13 | case id, type, tags, center, nodes, members, geometry, version, timestamp, changeset
14 | case latitude = "lat"
15 | case longitude = "lon"
16 | case userId = "uid"
17 | case username = "user"
18 | }
19 |
--------------------------------------------------------------------------------
/Source/Models/ElementsCodingKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ElementsCodingKeys.swift
3 | // OverpassApiVisualizer
4 | //
5 | // Created by Edward Samson on 10/13/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Coding key for the array of elements returned by an Overpass API request
12 | enum ElementsCodingKeys: CodingKey {
13 | case elements
14 | }
15 |
--------------------------------------------------------------------------------
/Source/Models/NestedPolygonCoordinates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NestedPolygonGeometry.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/14/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | // Used to store the coordinates for a nested polygon structure. The outer ring contains the coordinates for a single outer polygon. The inner ring contains the coordinates for any number of interior polygons.
12 | public struct NestedPolygonCoordinates {
13 |
14 | public let outerRing: [CLLocationCoordinate2D]
15 | public let innerRings: [[CLLocationCoordinate2D]]
16 |
17 | public init(
18 | outerRing: [CLLocationCoordinate2D],
19 | innerRings: [[CLLocationCoordinate2D]]
20 | ) {
21 | self.outerRing = outerRing
22 | self.innerRings = innerRings
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/Models/OPBoundingBox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPBoundingBox.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | // The 4 corner coordinates the define a search are of an Overpass API request.
12 | public struct OPBoundingBox {
13 |
14 | public let minLatitude: Double
15 | public let minLongitude: Double
16 | public let maxLatitude: Double
17 | public let maxLongitude: Double
18 |
19 | public init(
20 | minLatitude: Double,
21 | minLongitude: Double,
22 | maxLatitude: Double,
23 | maxLongitude: Double
24 | )
25 | {
26 | self.minLatitude = minLatitude
27 | self.minLongitude = minLongitude
28 | self.maxLatitude = maxLatitude
29 | self.maxLongitude = maxLongitude
30 | }
31 |
32 | public func toString() -> String {
33 |
34 | let commaSeparatedValues = [
35 | minLatitude,
36 | minLongitude,
37 | maxLatitude,
38 | maxLongitude
39 | ]
40 | .map {
41 | String($0)
42 | }
43 | .joined(separator: ",")
44 |
45 | return "(" + commaSeparatedValues + ")"
46 | }
47 | }
48 |
49 | // Convinience functions for creating a bounding box
50 | extension OPBoundingBox {
51 |
52 | // Creating from a mapkit region
53 | public init(region: MKCoordinateRegion) {
54 | let center = region.center
55 | let latitude = center.latitude
56 | let longitude = center.longitude
57 | let latitudeDelta = region.span.latitudeDelta
58 | let longitudeDelta = region.span.longitudeDelta
59 |
60 | minLatitude = latitude - latitudeDelta / 2
61 | maxLatitude = latitude + latitudeDelta / 2
62 |
63 | let minLongitude = longitude - longitudeDelta / 2
64 |
65 | // Preventing errors that may occur if the bounding box crosses the 180 degrees longitude line
66 | if minLongitude < -180 {
67 | self.minLongitude = 360 - minLongitude
68 | } else {
69 | self.minLongitude = minLongitude
70 | }
71 |
72 | let maxLongitude = longitude + longitudeDelta / 2
73 |
74 | // Preventing errors that may occur if the bounding box crosses the 180 degrees longitude line
75 | if maxLongitude > 180 {
76 | self.maxLongitude = maxLongitude - 360
77 | } else {
78 | self.maxLongitude = maxLongitude
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Source/Models/OPClientResult.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPClientResult.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // A generic class that either returns a successful result of the specified type or an error
12 | public enum OPClientResult {
13 | case success([Int: OPElement])
14 | case failure(Error)
15 | }
16 |
--------------------------------------------------------------------------------
/Source/Models/OPElementCenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPElementCenter.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/7/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | // Used to decode an element's center coordinate from an element's JSON dicitonary
12 | struct OPElementCenter {
13 |
14 | let latitude: Double
15 | let longitude: Double
16 | }
17 |
18 | extension OPElementCenter: Decodable {
19 | enum CodingKeys: String, CodingKey {
20 | case laitude = "lat"
21 | case longitude = "lon"
22 | }
23 |
24 | init(from decoder: Decoder) throws {
25 | let container = try decoder.container(keyedBy: CodingKeys.self)
26 | latitude = try container.decode(Double.self, forKey: .laitude)
27 | longitude = try container.decode(Double.self, forKey: .longitude)
28 | }
29 | }
30 |
31 | extension OPElementCenter {
32 | func toCoordinate() -> CLLocationCoordinate2D {
33 | return CLLocationCoordinate2D(
34 | latitude: latitude,
35 | longitude: longitude)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Source/Models/OPElementType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPElementType.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /*
12 | The types of elements that can be returned by an Overpass API request.
13 | Node: A single geographic point. Can be a single point of interest or part of a group of nodes that form higher order objects like ways or relations.
14 | Way: A collection of nodes that form a polylinear or polygonal geographic feature. Common examples include road ands buildings.
15 | Relation: A collection of related overpass members. Members can be nodes, ways (paths made up of nodes), and other relations.
16 | */
17 | public enum OPElementType: String, Codable {
18 | case node = "node"
19 | case way = "way"
20 | case relation = "relation"
21 |
22 | // The Overpass API language syntax for each element type.
23 | public var shortString: String {
24 | switch self {
25 | case .node:
26 | return "node"
27 | case .way:
28 | return "way"
29 | case .relation:
30 | return "rel"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Source/Models/OPGeometry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPGeometry.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/13/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | /*
12 | The types of geometries an element can have, represented in coordinates, a collection of coordinates, or in collections of coordinate collections. For example, a point of interest.
13 | Center: A single coordinate, or latitude/longitude pair.
14 | Polyline: An array of coordinates that form a polyline, or a collection of lines connected end-to-end. For example, a road.
15 | Polygon: Similar to a polyline except the first and last coordinates are the same. This is rendered as a closed polygon. For example, a building.
16 | Multipolygon: A collection of nested polygons. This can be used to create complicated clusters of polygons with internal voids. For example, a baseball statium.
17 | Multipolyline: A collection of polylines. For example, a collection of roads that make up the routes of a city's public transportation system.
18 | */
19 |
20 | public enum OPGeometry {
21 | case center(CLLocationCoordinate2D)
22 | case polyline([CLLocationCoordinate2D])
23 | case polygon([CLLocationCoordinate2D])
24 | case multiPolygon([NestedPolygonCoordinates])
25 | case multiPolyline([[CLLocationCoordinate2D]])
26 | case none
27 | }
28 |
--------------------------------------------------------------------------------
/Source/Models/OPMapKitVisualization.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPMapKitVisualization.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/7/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | /*
12 | Mapkit visualization types for overpass elements. Different returned elements require different visualization types.
13 | */
14 | public enum OPMapKitVisualization {
15 | case annotation(MKAnnotation)
16 | case polygon(MKPolygon)
17 | case polyline(MKPolyline)
18 | case polygons([MKPolygon])
19 | case polylines([MKPolyline])
20 | }
21 |
--------------------------------------------------------------------------------
/Source/Models/OPMeta.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPMeta.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Wolfgang Timme on 4/6/20.
6 | // Copyright © 2020 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Meta information about elements
12 | public struct OPMeta {
13 |
14 | /// OSM object's version number
15 | public let version: Int
16 |
17 | /// Last changed timestamp of an OSM object
18 | public let timestamp: String
19 |
20 | /// Changeset in which the object was changed
21 | public let changeset: Int
22 |
23 | /// OSM User id
24 | public let userId: Int
25 |
26 | /// OSM User name
27 | public let username: String
28 |
29 | public init(
30 | version: Int,
31 | timestamp: String,
32 | changeset: Int,
33 | userId: Int,
34 | username: String
35 | ) {
36 | self.version = version
37 | self.timestamp = timestamp
38 | self.changeset = changeset
39 | self.userId = userId
40 | self.username = username
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Source/Models/OPNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPNode.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/2/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | // A single geographic point. Can be a single point of interest or part of a group of nodes that form higher order objects like ways or relations.
12 | public struct OPNode: OPElement {
13 |
14 | public let id: Int
15 | public let tags: [String: String]
16 | public let isInteresting: Bool // Node contains an interesting tag it's description
17 | public var isSkippable: Bool // Node is already rendered by a parent way or relation
18 | public let geometry: OPGeometry // For nodes this will always be a single coordinate
19 | public let meta: OPMeta?
20 |
21 | public init(
22 | id: Int,
23 | tags: [String : String],
24 | isInteresting: Bool,
25 | isSkippable: Bool,
26 | geometry: OPGeometry,
27 | meta: OPMeta?
28 | ) {
29 | self.id = id
30 | self.tags = tags
31 | self.isInteresting = isInteresting
32 | self.isSkippable = isSkippable
33 | self.geometry = geometry
34 | self.meta = meta
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Source/Models/OPQueryOutputType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPQueryOutputType.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/11/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | /*
11 | Output types that effect the structure of JSON data returned by an Overpass API request.
12 |
13 | Standard: Returned elements contain refrence ids to their child elements.
14 |
15 | Recurse down: Standard output but query will return child objects for any parent element returned in the initial query. For example, querying a relation will also return any ways and/or nodes that are members in that relation.
16 |
17 | Recurse up: Standard output but query will return parent objects for any child element returned in the initial query. Fore example, querying for a node will also return any ways and/or relations that the node is a member of.
18 |
19 | Recurse up and down: Recurse up, then recurse down on the result of the upwards recursion.
20 |
21 | Geometry: Each returned element will contain a list of coorindates that define it's geometry.
22 |
23 | Center: Each returned element will contain it's center point. Use this if you are only concerned with representing the element as a single point or marker on a map.
24 | */
25 | public enum OPQueryOutputType {
26 | case standard, center, geometry, recurseDown, recurseUp, recurseUpAndDown
27 |
28 | // The Overpass API language syntax for each output type
29 | func toString() -> String {
30 | switch self {
31 | case .standard:
32 | return "out;"
33 | case .recurseDown:
34 | return "(._;>;);out;"
35 | case .recurseUp:
36 | return "(._;<;);out;"
37 | case .recurseUpAndDown:
38 | return "((._;<;);>;);out;"
39 | case .geometry:
40 | return "out geom;"
41 | case .center:
42 | return "out center;"
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Source/Models/OPRelation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPRelation.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/2/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | // A collection of related overpass members. Members can be nodes, ways (paths made up of nodes), and other relations.
12 | public struct OPRelation: OPElement {
13 |
14 | public struct Member {
15 |
16 | // Used to decode the member from an Overpass API JSON response.
17 | enum CodingKeys: String, CodingKey {
18 | case type, role, geometry, nodes
19 | case id = "ref"
20 | }
21 |
22 | public let type: OPElementType // The member's type
23 | public let id: Int // The member's unique identifier
24 | public let role: String // The role a member playes in the relation
25 | public let coordinates: [CLLocationCoordinate2D] // The coordinates of the member
26 |
27 | public init(
28 | type: OPElementType,
29 | id: Int,
30 | role: String,
31 | coordinates: [CLLocationCoordinate2D]
32 | ) {
33 | self.type = type
34 | self.id = id
35 | self.role = role
36 | self.coordinates = coordinates
37 | }
38 | }
39 |
40 | public let id: Int
41 | public let tags: [String: String]
42 | public let isInteresting: Bool // Relatin contains an interesting descriptive tag
43 | public var isSkippable: Bool // Relation is already rendered by a parent element
44 | public let members: [Int] // Members that form the relation
45 | public let geometry: OPGeometry // The relation's geometry type
46 | public let meta: OPMeta?
47 |
48 | public init(
49 | id: Int,
50 | tags: [String : String],
51 | isInteresting: Bool,
52 | isSkippable: Bool, members: [Int],
53 | geometry: OPGeometry,
54 | meta: OPMeta?
55 | ) {
56 | self.id = id
57 | self.tags = tags
58 | self.isInteresting = isInteresting
59 | self.isSkippable = isSkippable
60 | self.members = members
61 | self.geometry = geometry
62 | self.meta = meta
63 | }
64 | }
65 |
66 | extension OPRelation {
67 | // Many relations are just collections of related objects, but these relation types require specific renderings
68 | static let displayableTypes: Set = [
69 | Overpass.Values.multipolygon,
70 | Overpass.Values.barrier,
71 | Overpass.Values.route,
72 | Overpass.Values.waterway
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/Source/Models/OPWay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPWay.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/2/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import MapKit
10 |
11 | // A collection of nodes that form a polylinear or polygonal geographic feature. Common examples include road ands buildings.
12 | public struct OPWay: OPElement{
13 |
14 | public let id: Int
15 | public let tags: [String: String]
16 | public let isInteresting: Bool // Way has interesting tags in it's description
17 | public var isSkippable: Bool // Way is already rendered by a parent relation
18 | public let nodes: [Int] // Nodes for each coordinate in a way's geometry
19 | public let geometry: OPGeometry // For a way this will be either a polyline or a polygon
20 | public let meta: OPMeta?
21 |
22 | public init(
23 | id: Int,
24 | tags: [String : String],
25 | isInteresting: Bool,
26 | isSkippable: Bool,
27 | nodes: [Int],
28 | geometry: OPGeometry,
29 | meta: OPMeta?
30 | ) {
31 | self.id = id
32 | self.tags = tags
33 | self.isInteresting = isInteresting
34 | self.isSkippable = isSkippable
35 | self.nodes = nodes
36 | self.geometry = geometry
37 | self.meta = meta
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Source/Networking/OPClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPClient.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | // A class for making requests to an Overpass API endpoint and decoding the subsequent response
12 | public class OPClient {
13 |
14 | /*
15 | These are the endpoints listed at:
16 | https://wiki.openstreetmap.org/wiki/Overpass_API
17 |
18 | Users can also define a custom endpoint.
19 | */
20 | public enum Endpoint {
21 | case main, main2, french, swiss, kumiSystems, taiwan
22 | case custom(urlString: String)
23 |
24 | public var urlString: String {
25 | switch self {
26 | case .main:
27 | return "https://lz4.overpass-api.de/api/interpreter"
28 | case .main2:
29 | return "https://z.overpass-api.de/api/interpreter"
30 | case .french:
31 | return "http://overpass.openstreetmap.fr/api/interpreter"
32 | case .swiss:
33 | return "http://overpass.osm.ch/api/interpreter"
34 | case .kumiSystems:
35 | return "https://overpass.kumi.systems/api/interpreter"
36 | case .taiwan:
37 | return "https://overpass.nchc.org.tw"
38 | case .custom(let urlString):
39 | return urlString
40 | }
41 | }
42 | }
43 |
44 | private let session: URLSession
45 |
46 | // Store a reference to any url task being performed in case it needs to be cancelled.
47 | private var task: URLSessionDataTask?
48 |
49 | // Store the current query
50 | private var query: String? = nil
51 |
52 | // The selected endpoint for the overpass api post request
53 | public var endpoint: Endpoint
54 |
55 | // Getting and setting the url string. Has the same effect as setting the endpoint.
56 | public var endpointUrlString: String {
57 | set {
58 | self.endpoint = .custom(urlString: newValue)
59 | }
60 | get {
61 | return endpoint.urlString
62 | }
63 | }
64 |
65 | // The queue on which decoding operations are run
66 | private lazy var elementDecodingQueue: OperationQueue = {
67 | var queue = OperationQueue()
68 | queue.name = "Element decoding queue"
69 | queue.maxConcurrentOperationCount = 1
70 | return queue
71 | }()
72 |
73 | // Initializing the client with an endpoint and a url session. I've found the kumi systems endpoint to be the least restrictive in terms of usage.
74 | public init(
75 | endpoint: Endpoint = .kumiSystems,
76 | session: URLSession = URLSession.shared)
77 | {
78 | self.session = session
79 | self.endpoint = endpoint
80 |
81 | }
82 |
83 | // Initialized a client with an endpoint url string and a url session
84 | public init(
85 | endpointUrlString: String,
86 | session: URLSession = URLSession.shared)
87 | {
88 | self.session = session
89 | self.endpoint = .custom(urlString: endpointUrlString)
90 | }
91 |
92 | // A fetch request to the api. Requires a query that is written in the Overpass API language. For simple queries, the OverpassQueryBuilder class can be used to conviniently build queries.
93 | public func fetchElements(
94 | query: String,
95 | completion: @escaping (OPClientResult) -> Void)
96 | {
97 | // Store the current query and cancel any ongoing fetches by the client
98 | self.query = query
99 | cancelFetch()
100 |
101 | // Convert the endpoint URL string into a URL
102 | guard let url = URL(string: endpointUrlString) else {
103 | return
104 | }
105 |
106 | // encode the query string into data
107 | let data = query.data(using: .utf8)
108 |
109 | // Build the Overpass API request. The request posts your data containing the Overpass API code to the client's endpoint
110 | var request = URLRequest(url: url)
111 | request.httpMethod = "POST"
112 | request.httpBody = data
113 |
114 | // Sending the URL request
115 | task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
116 |
117 | // Peform the completion handler on the main thread
118 | let completionOnMain: (OPClientResult) -> Void = { result in
119 | DispatchQueue.main.async {
120 | completion(result)
121 | }
122 | }
123 |
124 | // Remove the stored reference to the task
125 | self?.task = nil
126 |
127 | // If the stored query has changed since last request finished, ignore the finished request.
128 | guard self?.query == query else {
129 | return
130 | }
131 |
132 | // If the response returned an error, abort
133 | if let error = error {
134 | completionOnMain(.failure(error))
135 | return
136 | }
137 |
138 | // If an unsuccessful response is recieved, abort
139 | if
140 | let httpResponse = response as? HTTPURLResponse,
141 | httpResponse.statusCode != 200
142 | {
143 | completionOnMain(.failure(OPRequestError.badResponse(httpResponse)))
144 | return
145 | }
146 |
147 | // If the request returned nil data, abort
148 | guard let data = data else {
149 | completionOnMain(.failure(OPRequestError.nilData))
150 | return
151 | }
152 |
153 | // initialize an operation to return the decoded data
154 | let operation = OPDecodingOperation(data: data)
155 |
156 | // On completion of the operation, if no errors are thrown and the oepration wasn't cancelled, pass the decoded elements to the completion handler.
157 | operation.completionBlock = {
158 |
159 | DispatchQueue.main.async {
160 | if operation.isCancelled {
161 | return
162 | }
163 | if let error = operation.error {
164 | completion(.failure(error))
165 | return
166 | }
167 | completion(.success(operation.elements))
168 | }
169 | }
170 |
171 | // Queue up the operation
172 | self?.elementDecodingQueue.addOperation(operation)
173 | }
174 |
175 | // Run the asynchronous request to the Overpass API endpoint
176 | task?.resume()
177 | }
178 |
179 | // Cancel the current fetch/decoding operation
180 | public func cancelFetch() {
181 | task?.cancel()
182 | elementDecodingQueue.cancelAllOperations()
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/Source/Networking/OPDecodingOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPDecodingOperation.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/13/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | /*
12 | An operation that decodes the JSON response from an Overpass API request in a dictionary of elements (nodes, ways, and relations). I chose to make decoding an operation so that if a client makes a newer query it can cancel decoding the old query and the new query's decoding process won't start until the cancelled decoding process has been removed.
13 | */
14 |
15 | class OPDecodingOperation: Operation {
16 |
17 | private var _elements = [Int: OPElement]() // Decoded Overpass elements from JSON data
18 | private var _error: Error?
19 | private let data: Data // Data to be decoded
20 |
21 | // Decoded elements can only be read after the operation is finished. Not sure if this is neccesary for safety, but it prevents the elements dictionary from being read from a different thread while writes are occuring on the operation's thread.
22 | var elements: [Int: OPElement] {
23 | guard isFinished else {
24 | return [:]
25 | }
26 | return _elements
27 | }
28 |
29 | var error: Error? {
30 | guard isFinished else {
31 | return nil
32 | }
33 | return _error
34 | }
35 |
36 | // Operation is initialized w/ data to be decoded
37 | init(data: Data) {
38 | self.data = data
39 | }
40 |
41 | // Main function of the operation
42 | override func main() {
43 |
44 | do {
45 | try decodeElements(from: data)
46 | } catch {
47 | self._error = error
48 | }
49 | }
50 |
51 |
52 | private func decodeElements(from data: Data) throws {
53 |
54 | // JSON decoder for decoding response
55 | let jsonDecoder = JSONDecoder()
56 |
57 | // In order to create containers for decoding our data outside of the init(from: Decoder) function, we have to do something a little hackey and create a struct with the sole purpose of initializing a decoder that holds our JSON data.
58 | let decoderExtractor = try jsonDecoder.decode(DecoderExtractor.self, from: data)
59 | let decoder = decoderExtractor.decoder
60 |
61 | // Container for the entire JSON response
62 | let container = try decoder.container(keyedBy: ElementsCodingKeys.self)
63 |
64 | // Nested container for the elements array in our JSON response, we will step through this container to decode each element in the array.
65 | let elementsContainer = try container.nestedUnkeyedContainer(
66 | forKey: .elements)
67 |
68 | let elementTypes: [OPElementType] = [.node, .way, .relation]
69 |
70 | for elementType in elementTypes {
71 |
72 | // Because the geometry construction process requires the decoding of nodes first, arrays second, and relations last, we need to step through our elements container 3 times in total to decode everything in the correct order. In each decoding pass through the elements array, the unkeyed container decodes the element at the current index and then automatically steps through to the next index until it reaches the end. Since there is no way to reset a container's index to zero and make another pass through the array after the container reaches the end index, we have to create a copy of the elements container for each individual decoding pass we make. At some point I'll explore alternatives to the multiple containers/decoding passes approach I am using now. It may be better to decode everything first and then construct the geometries afterwards.
73 | var elementsContainerCopy = elementsContainer
74 |
75 | while !elementsContainerCopy.isAtEnd {
76 |
77 | if isCancelled {
78 | return
79 | }
80 |
81 | do {
82 | // Create a nested container for each individual element in the array, this automatically moves the current index of the parent container to the next index in the elements array.
83 | let elementContainer = try elementsContainerCopy.nestedContainer(
84 | keyedBy: ElementCodingKeys.self)
85 |
86 | // Find the element's type using the nested container. Skip the decoding process if the element's type should not be decoded for this step.
87 | let type = try elementContainer.decode(OPElementType.self, forKey: .type)
88 | guard elementType == type else { continue }
89 |
90 | // Decode the element
91 | let element: OPElement
92 |
93 | switch elementType {
94 | case .node:
95 | element = try decodeNode(within: elementContainer)
96 | case .way:
97 | element = try decodeWay(within: elementContainer)
98 | case .relation:
99 | element = try decodeRelation(within: elementContainer)
100 | }
101 |
102 | // Add to elements dictionary if decoding was successful
103 | _elements.updateValue(element, forKey: element.id)
104 | } catch {
105 | // We want to catch any decoding errors for individual elements so a bad element does not stop the entire decoding process.
106 | print(error.localizedDescription)
107 | }
108 | }
109 | }
110 | }
111 |
112 | // A function for decoding a node inside of a keyed container. This could be done by adding an init(from: Decoder) to our nodes class, but since we can't do the same for Ways and Relations (they require information outside of the decoder to be constructed) I opted for to do it in a separate function to keep things consstent.
113 | private func decodeNode(within container: KeyedDecodingContainer) throws -> OPNode {
114 |
115 | // Decode the node's id number
116 | let id = try container.decode(Int.self, forKey: .id)
117 |
118 | // If present, decode the tag dictionary that provides additional element details. Then check to see if any interesting tags are present. The isInteresting property is useful for determining whether or not to plot an element.
119 | let tags = try container.decodeIfPresent([String: String].self, forKey: .tags) ?? [:]
120 | let isInteresting = TagChecker.checkForInterestingTags(amongstTags: tags)
121 |
122 | // Decode the coordinate of the node
123 | let latitude = try container.decode(Double.self, forKey: .latitude)
124 | let longitude = try container.decode(Double.self, forKey: .longitude)
125 | let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
126 |
127 | // Decode the optional meta information of the node
128 | let meta = try? decodeMeta(within: container)
129 |
130 | // Return the decoded node
131 | return OPNode(
132 | id: id,
133 | tags: tags,
134 | isInteresting: isInteresting,
135 | isSkippable: false,
136 | geometry: .center(coordinate),
137 | meta: meta)
138 | }
139 |
140 | // A function for decoding a way inside of a keyed container.
141 | private func decodeWay(within container: KeyedDecodingContainer) throws -> OPWay {
142 |
143 | // Decode the way's id number
144 | let id = try container.decode(Int.self, forKey: .id)
145 |
146 | // If present, decode the tag dictionary that provides additional element details. Then check to see if any interesting tags are present. The isInteresting property is useful for determining whether or not to plot an element.
147 | let tags = try container.decodeIfPresent([String: String].self, forKey: .tags) ?? [:]
148 | let isInteresting = TagChecker.checkForInterestingTags(amongstTags: tags)
149 |
150 | // Decode the ids of each child node make denotes a coorindate in the way's geometry
151 | let nodes = try container.decode([Int].self, forKey: .nodes)
152 |
153 | // A coordinate or collection of coordinates that describes the way's geometry. Varies depending on the output format of the query.
154 | let geometry: OPGeometry
155 |
156 | // If center was specified as the output format, the center key will be present in the way's JSON dictionary. If present, decode the way's center and make the geometry a single coordinate.
157 | if let center = try container.decodeIfPresent(
158 | OPElementCenter.self,
159 | forKey: .center)
160 | {
161 | let coordinate = CLLocationCoordinate2D(
162 | latitude: center.latitude,
163 | longitude: center.longitude)
164 |
165 | geometry = .center(coordinate)
166 |
167 | } else {
168 | // If center was not specified, we will attempt to create the way's full geometry.
169 |
170 | let coordinates: [CLLocationCoordinate2D]
171 |
172 | // If the output type geometry was specified, the full geometry will already be present in the geometry key of the way's dictionary.
173 | if let fullGeometry = try container.decodeIfPresent(
174 | [[String: Double]].self,
175 | forKey: .geometry)
176 | {
177 | // after decoding the geometry array, we convert it too an array of coordinates
178 | coordinates = fullGeometry.compactMap {
179 | guard
180 | let latitude = $0[Overpass.Keys.latitude],
181 | let longitude = $0[Overpass.Keys.longitude]
182 | else {
183 | return nil
184 | }
185 | return CLLocationCoordinate2D(
186 | latitude: latitude,
187 | longitude: longitude)
188 | }
189 |
190 | } else {
191 |
192 | // If full geometry was not specified, we will attempt to get the coordinates for each of the way's points from previously decoded nodes. This is why I've chosen to decode all of the nodes prior to decoding the ways. I've tried decoding all of the element types in one step and then constructing the geometries later, but this felt slightly cleaner to me.
193 | coordinates = nodes.compactMap {
194 | // If the element's geometry is a single coordinate, add it too the way's geometry array. Otherwise, return nil
195 | guard case .center(let coordinate) = _elements[$0]?.geometry else {
196 | return nil
197 | }
198 | return coordinate
199 | }
200 | }
201 |
202 | // If there was an error generating any of the way's child coordinates, throw an error
203 | guard coordinates.count == nodes.count else {
204 | throw OPElementDecoderError.invalidWayLength(wayId: id)
205 | }
206 |
207 | // Check whether or not the way is a polygon, set the geometry enum accordingly.
208 | let isPolygon = PolygonChecker.checkWay(
209 | withCoordinates: coordinates,
210 | andTags: tags)
211 |
212 | if isPolygon {
213 | geometry = .polygon(coordinates)
214 | } else {
215 | geometry = .polyline(coordinates)
216 | }
217 | }
218 |
219 | // Decode the optional meta information of the way
220 | let meta = try? decodeMeta(within: container)
221 |
222 | // Return the decoded way
223 | return OPWay(
224 | id: id,
225 | tags: tags,
226 | isInteresting: isInteresting,
227 | isSkippable: false,
228 | nodes: nodes,
229 | geometry: geometry,
230 | meta: meta)
231 | }
232 |
233 | // A function for decoding a relation inside of a keyed container.
234 | private func decodeRelation(
235 | within container: KeyedDecodingContainer) throws -> OPRelation
236 | {
237 | // Decode the way's id number
238 | let id = try container.decode(Int.self, forKey: .id)
239 |
240 | // If present, decode the tag dictionary that provides additional element details. Then check to see if any interesting tags are present. The isInteresting property is useful for determining whether or not to plot an element.
241 | let tags = try container.decodeIfPresent([String: String].self, forKey: .tags) ?? [:]
242 | let isInteresting = TagChecker.checkForInterestingTags(amongstTags: tags)
243 |
244 | // Get the relation type
245 | let relationType = tags[Overpass.Keys.type]
246 |
247 | // If center was specified as the output format, the center key will be present in the relation's JSON dictionary. If present, decode the relation's center.
248 | let center = try container.decodeIfPresent(OPElementCenter.self, forKey: .center)
249 | let centerIsPresent = center != nil
250 |
251 | let isDisplayable: Bool
252 |
253 | // If the relation has a displayable type, set isDisplayable to true. This will be determine whether the relation should have an associated geometery.
254 | if let relationType = relationType {
255 | isDisplayable = OPRelation.displayableTypes.contains(relationType)
256 | } else {
257 | isDisplayable = false
258 | }
259 |
260 | // A relation is a collection of members. Members can be nodes, ways, or relations.
261 | var members: [OPRelation.Member]
262 |
263 | // A container that will be used to decode the relation's member array.
264 | var membersContainer = try container.nestedUnkeyedContainer(forKey: .members)
265 |
266 | if isDisplayable && !centerIsPresent {
267 | // If full geometry is required, decode members and generate their geometries.
268 | members = try decodeDisplayableRelationMembers(within: &membersContainer)
269 | } else {
270 | // Otherwise, decode the non-gemoetric data of the members only
271 | members = try decodeRelationMembers(within: &membersContainer)
272 | }
273 |
274 | // Generate an array of member ids.
275 | let memberIds = members.map { $0.id }
276 |
277 | // Initializing the geometry variable for the relation.
278 | let geometry: OPGeometry
279 |
280 | // A relation's geometry is a multipolygon if it has one of the following types:
281 | let isMultiPolygon =
282 | relationType == Overpass.Values.multipolygon ||
283 | relationType == Overpass.Values.barrier
284 |
285 | if centerIsPresent {
286 | if isMultiPolygon {
287 | // If the output type is center and the relation type is multipolygon
288 |
289 | guard let center = center else {
290 | throw OPElementDecoderError.unexpectedNil(elementId: id)
291 | }
292 |
293 | // Denote that each member is skippable, otherwise multiple annotations would correspond to the same object
294 | members.map { $0.id }.forEach { memberId in
295 | _elements[memberId]?.isSkippable = true
296 | }
297 |
298 | // Get the center coordinate from the decoded center object
299 | let coordinate = CLLocationCoordinate2D(
300 | latitude: center.latitude,
301 | longitude: center.longitude)
302 |
303 | // Set the geometry to center
304 | geometry = .center(coordinate)
305 |
306 | } else {
307 | // If the output type is center ut the relation isn't a multipolygon do not set any geometry
308 | geometry = .none
309 | }
310 | } else if isMultiPolygon {
311 | // If the relation is a multipolygon generate theappropriate geometry (an array of nested polygon coordinates)
312 | geometry = try generateMultiPolygonGeometry(fromMembers: members)
313 | } else if isDisplayable {
314 | // If the relation is displayable but not a multipolygon, it's geometry is a multipolyline
315 | geometry = try generateMultiPolylineGeometry(fromMembers: members)
316 | } else {
317 | // Otherwise, the relation should not have any geometry.
318 | geometry = .none
319 | }
320 |
321 | // Decode the optional meta information of the relation
322 | let meta = try? decodeMeta(within: container)
323 |
324 | // Return the decoded relation
325 | return OPRelation(
326 | id: id,
327 | tags: tags,
328 | isInteresting: isInteresting,
329 | isSkippable: false,
330 | members: memberIds,
331 | geometry: geometry,
332 | meta: meta)
333 | }
334 |
335 | // Decode relation members w/o setting their geometries
336 | private func decodeRelationMembers(within container: inout UnkeyedDecodingContainer) throws -> [OPRelation.Member] {
337 |
338 | var members = [OPRelation.Member]()
339 |
340 | while !container.isAtEnd {
341 |
342 | // Use a keyed container to decode the id, type, and role of each relation member
343 | let memberContainer = try container.nestedContainer(keyedBy: OPRelation.Member.CodingKeys.self)
344 | let id = try memberContainer.decode(Int.self, forKey: .id)
345 | let type = try memberContainer.decode(OPElementType.self, forKey: .type)
346 | let role = try memberContainer.decode(String.self, forKey: .role)
347 |
348 | // Generate the member object w/ an empty coordinate array
349 | let member = OPRelation.Member(
350 | type: type,
351 | id: id,
352 | role: role,
353 | coordinates: [])
354 |
355 | members.append(member)
356 | }
357 |
358 | return members
359 | }
360 |
361 | // Decode relations and set the geometry
362 | private func decodeDisplayableRelationMembers(within container: inout UnkeyedDecodingContainer) throws -> [OPRelation.Member] {
363 |
364 | var members = [OPRelation.Member]()
365 |
366 | while !container.isAtEnd {
367 |
368 | // Use a keyed container to decode the id, type, and role of each relation member
369 | let memberContainer = try container.nestedContainer(keyedBy: OPRelation.Member.CodingKeys.self)
370 | let id = try memberContainer.decode(Int.self, forKey: .id)
371 | let type = try memberContainer.decode(OPElementType.self, forKey: .type)
372 | let role = try memberContainer.decode(String.self, forKey: .role)
373 |
374 | // Generate the geometries for each member
375 | let coordinates: [CLLocationCoordinate2D]
376 |
377 | // If the geometry output is specified, each member will contain an array of coordinates
378 | if let fullGeometry = try memberContainer.decodeIfPresent([[String: Double]].self, forKey: .geometry) {
379 |
380 | // Map the coordinate array to an array of CLLocationCoordinate2d
381 | coordinates = fullGeometry.compactMap {
382 | guard
383 | let latitude = $0[Overpass.Keys.latitude],
384 | let longitude = $0[Overpass.Keys.longitude]
385 | else {
386 | return nil
387 | }
388 | return CLLocationCoordinate2D(
389 | latitude: latitude,
390 | longitude: longitude)
391 | }
392 |
393 | // If any coordiantes are missing after the conversion, disregard the member
394 | guard coordinates.count == fullGeometry.count else { continue }
395 |
396 | } else {
397 | //If the output isn't geometry or center, construct the geometries of member ways from previously decoded ways
398 | let id = try memberContainer.decode(Int.self, forKey: .id)
399 |
400 | // If the member is a way, set it's geometry
401 | guard let element = _elements[id] else {
402 | continue
403 | }
404 |
405 | // If the member is a way set, the geometry to the way's coordinates
406 | switch element.geometry {
407 | case .polygon(let wayCoordinates), .polyline(let wayCoordinates):
408 | coordinates = wayCoordinates
409 | default:
410 | coordinates = []
411 | }
412 | }
413 |
414 | // Generate a new member object with a full geometry if it is a way
415 | let member = OPRelation.Member(
416 | type: type,
417 | id: id,
418 | role: role,
419 | coordinates: coordinates)
420 |
421 | members.append(member)
422 | }
423 |
424 | // Return all members
425 | return members
426 | }
427 |
428 | // A function for decoding optional meta information inside of a keyed container.
429 | private func decodeMeta(within container: KeyedDecodingContainer) throws -> OPMeta {
430 | let version = try container.decode(Int.self, forKey: .version)
431 | let timestamp = try container.decode(String.self, forKey: .timestamp)
432 | let changeset = try container.decode(Int.self, forKey: .changeset)
433 | let userId = try container.decode(Int.self, forKey: .userId)
434 | let username = try container.decode(String.self, forKey: .username)
435 |
436 | return OPMeta(version: version,
437 | timestamp: timestamp,
438 | changeset: changeset,
439 | userId: userId,
440 | username: username)
441 | }
442 |
443 | // Generate the geometry for multipolygons
444 | private func generateMultiPolygonGeometry(
445 | fromMembers members: [OPRelation.Member]) throws -> OPGeometry
446 | {
447 |
448 | // Get the inner and out member ways
449 | let memberWays = members.filter { $0.type == .way }
450 | let outerWays = memberWays.filter { $0.role == Overpass.Values.outer }
451 | let innerWays = memberWays.filter { $0.role == Overpass.Values.inner }
452 |
453 | // Get an array where each element is a member way's coordinates
454 | let outerCoordinateArrays = outerWays.map { $0.coordinates }
455 | let innerCoordinateArrays = innerWays.map { $0.coordinates }
456 |
457 | // Merge the ways with matching end coordinates
458 | let mergedOuterWays = merge(coordinateArrays: outerCoordinateArrays)
459 | let mergedInnerWays = merge(coordinateArrays: innerCoordinateArrays)
460 |
461 | // Match inner ways with outer ways. An outer way can have any number of inner ways
462 | let geometries = assembleNestedGeometries(
463 | outerGeometries: mergedOuterWays,
464 | innerGeometries: mergedInnerWays)
465 |
466 | // If a multipolygon contains to ways, than it has no geometry
467 | guard !geometries.isEmpty else {
468 | throw OPElementDecoderError.emptyRelation
469 | }
470 |
471 | // Denote that the outer and inner ways of the multipolygon are skippable so they are not rendered individually in addition to being rendered as a multipolygon.
472 | outerWays.forEach {
473 | _elements[$0.id]?.isSkippable = true
474 | }
475 |
476 | innerWays.forEach {
477 | _elements[$0.id]?.isSkippable = true
478 | }
479 |
480 | // return the geometry
481 | return .multiPolygon(geometries)
482 | }
483 |
484 | // Generate the geometry for multipolylines
485 | private func generateMultiPolylineGeometry(
486 | fromMembers members: [OPRelation.Member]) throws -> OPGeometry
487 | {
488 | // Filter out members for ways only and assemble an array where each element is a member way's coordinates
489 | let memberWays = members.filter { $0.type == .way }
490 | let wayGeometries = memberWays.map { $0.coordinates }
491 |
492 | // Merge ways with matching end coordinates
493 | let mergedWayGeometries = merge(coordinateArrays: wayGeometries)
494 |
495 | guard !mergedWayGeometries.isEmpty else {
496 | throw OPElementDecoderError.emptyRelation
497 | }
498 |
499 | // Interesting ways can be rendered individually. Uninteresting ways are skippable if already rendered by the mulipolyline
500 | memberWays.forEach {
501 | if _elements[$0.id]?.isInteresting != true {
502 | _elements[$0.id]?.isSkippable = true
503 | }
504 | }
505 |
506 | // Return the multipolyline
507 | return .multiPolyline(mergedWayGeometries)
508 | }
509 |
510 | // Merge ways end to end to form larger geometries
511 | private func merge(coordinateArrays: [[CLLocationCoordinate2D]]) -> [[CLLocationCoordinate2D]] {
512 |
513 | var geometries = coordinateArrays // Initial unmerged way geometries
514 | var mergedGeometries = [[CLLocationCoordinate2D]]() // Array for storing merged geometries
515 |
516 | // A linked list for storing geometries to merge.
517 | let geometriesToMerge = LinkedList<[CLLocationCoordinate2D]>()
518 |
519 | while true {
520 |
521 | // Pop the last unmerged geometry from the geometries array. We will attempt to merge additional geometries to it
522 | guard let geometry = geometries.popLast() else {
523 | break
524 | }
525 |
526 | geometriesToMerge.append(value: geometry)
527 |
528 | // Get the first and last coordinate of the soon to be merged geometries
529 | guard
530 | let mergedFirst = geometriesToMerge.first?.value.first, // First element of first node in linked list
531 | let mergedLast = geometriesToMerge.last?.value.last // Last element of last node in linked list
532 | else {
533 | continue
534 | }
535 |
536 | // If there are geometries available to merge and if current geometries pending merger dont' form a closed loop, attempt to add another geometry to the geometries to merge list
537 | while !geometries.isEmpty || mergedFirst.isEqual(to: mergedLast) {
538 |
539 | // Get the number of to be merged geometries prior to merging
540 | let mergedLength = geometriesToMerge.count
541 |
542 | // Check unmerged arrays to see if any have matching ends with out base geometry
543 | for (index, currentGeometry) in geometries.enumerated() {
544 |
545 | // Get the endpoints for the geometry we are attempting to mergo to our base
546 | guard
547 | let currentFirst = currentGeometry.first,
548 | let currentLast = currentGeometry.last
549 | else {
550 | continue
551 | }
552 |
553 | if mergedLast.isEqual(to: currentFirst) {
554 | // If the last coordinate of the first geometry matches the the first coordinate of second geometry, merge the two arrays without doing any preprocessing
555 | geometriesToMerge.last?.value.removeLast()
556 | geometriesToMerge.append(value: geometries.remove(at: index))
557 | break
558 | } else if mergedLast.isEqual(to: currentLast) {
559 | // If the the last coordinate of the first geometry matches the last coordinate of the second geometry, reverse the second geometry and merge the coordinate arrays.
560 | geometriesToMerge.last?.value.removeLast()
561 | geometriesToMerge.append(value: geometries.remove(at: index).reversed())
562 | break
563 | } else if mergedFirst.isEqual(to: currentLast) {
564 | // If the first element of the first geometry matches the last element of the second geometry, append the second geometry to the start of the first geometry.
565 | var geometryToAdd = geometries.remove(at: index)
566 | geometryToAdd.removeLast()
567 | geometriesToMerge.insert(value: geometryToAdd, atIndex: 0)
568 | break
569 | } else if mergedFirst.isEqual(to: currentFirst) {
570 | // If the first element of the first geometry matches the first element of the second geometry, reverse the second geometry and appent it to the front of the first geometry.
571 | var geometryToAdd = geometries.remove(at: index)
572 | geometryToAdd.reverse()
573 | geometryToAdd.removeLast()
574 | geometriesToMerge.insert(value: geometryToAdd, atIndex: 0)
575 | break
576 | }
577 | }
578 |
579 | // If no geometries were able to be merged to our base geometry, break and start over with another base geometry. If we were able to grow our base geometry to a meger, attempt to merge additional geometries onto our base geometry until no matches can be found or the base geometry forms a closed loop.
580 | if mergedLength == geometriesToMerge.count {
581 | break
582 | }
583 | }
584 |
585 | // Append a geometry to the merged geometries output array once no more mergers can be made to it.
586 | mergedGeometries.append(geometriesToMerge.mergedCoordinateList())
587 |
588 | // Remove all geometries from geometries to merge list
589 | geometriesToMerge.removeAll()
590 | }
591 |
592 | //output the merged geometries
593 | return mergedGeometries
594 | }
595 |
596 | // A function that matches inner geometries with outer geometries by determining whether or not an inner geometry has a coordinate that exists within any of the supplied outer geometries.
597 | private func assembleNestedGeometries(
598 | outerGeometries: [[CLLocationCoordinate2D]],
599 | innerGeometries: [[CLLocationCoordinate2D]]) -> [NestedPolygonCoordinates]
600 | {
601 | // Filter out any outer geometries that do not form a closed loop
602 | let outerRings = outerGeometries.filter {
603 | guard
604 | let firstCoordinate = $0.first,
605 | let lastCoordinate = $0.last
606 | else {
607 | return false
608 | }
609 | return firstCoordinate.isEqual(to: lastCoordinate)
610 |
611 | }
612 |
613 | // Filter out any inner geometries that do not form a closed loop
614 | let innerRings = innerGeometries.filter {
615 | guard
616 | let firstCoordinate = $0.first,
617 | let lastCoordinate = $0.last
618 | else {
619 | return false
620 | }
621 | return firstCoordinate.isEqual(to: lastCoordinate)
622 |
623 | }
624 |
625 | // Initialize an array of nested polygons in coordinate form
626 | var geometries = [NestedPolygonCoordinates]()
627 |
628 | // Intilialize a set for tracking which inner geometries have already been matched to an outer geometry
629 | var consumedInnerGeometryIndices = Set()
630 |
631 | // For each outer ring of coordinates
632 | for outer in outerRings {
633 |
634 | // Initialize an array of inner rings that match with that outer ring
635 | var innersForOuter = [[CLLocationCoordinate2D]]()
636 |
637 | // For each inner array
638 | for (index, inner) in innerRings.enumerated() {
639 |
640 | // If it has not already been matched with an outer ring
641 | guard !consumedInnerGeometryIndices.contains(index) else {
642 | continue
643 | }
644 |
645 | // If the inner ring is empty consume it without adding it to an outer ring
646 | guard
647 | let coordinate = inner.first
648 | else {
649 | consumedInnerGeometryIndices.insert(index)
650 | continue
651 | }
652 |
653 | // Check to see if the inner ring has a coordinate within the current outer ring. If true, consume the inner ring and append it to the inner rings array for the current outer ring.
654 | if checkForCoordinate(coordinate, inPolygonFormedByCoordinates: outer) {
655 | innersForOuter.append(inner)
656 | consumedInnerGeometryIndices.insert(index)
657 | }
658 | }
659 |
660 | // Create a nested polygon coordinates object from the current outer ring and any inner rings that it contains
661 | let nestedGeometry = NestedPolygonCoordinates(
662 | outerRing: outer,
663 | innerRings: innersForOuter)
664 |
665 | // append it to the nested geometry array
666 | geometries.append(nestedGeometry)
667 | }
668 |
669 | // return all nested geometries
670 | return geometries
671 | }
672 |
673 | // Ray casting algroithm to determine whether or not a coordinate is found withing a polygon whose vertices are formed by an array of polygon coordinates. If a line formed between the coordiante of interest and any point outside the polygon intersects with the polygon's perimeter an odd number of times then it is within the polygon.
674 | private func checkForCoordinate(
675 | _ c: CLLocationCoordinate2D,
676 | inPolygonFormedByCoordinates polygonCoordinates: [CLLocationCoordinate2D]) -> Bool
677 | {
678 | var isInside = false
679 |
680 | for index in 0..<(polygonCoordinates.count - 1) {
681 | let c1 = polygonCoordinates[index]
682 | let c2 = polygonCoordinates[index + 1]
683 |
684 | if
685 | ((c1.latitude > c.latitude) != (c2.latitude > c.latitude)) &&
686 | (c.longitude < (c2.longitude - c1.longitude) * (c.latitude - c1.longitude) / (c2.latitude - c1.latitude) + c1.longitude)
687 | {
688 | isInside = !isInside
689 | }
690 | }
691 | return isInside
692 | }
693 | }
694 |
695 | fileprivate extension LinkedList where T == Array {
696 |
697 | func mergedCoordinateList() -> [T.Element] {
698 | guard var node = first else {
699 | return []
700 | }
701 |
702 | var merged: T = node.value
703 |
704 | while let next = node.next {
705 | merged.append(contentsOf: next.value)
706 | node = next
707 | }
708 | return merged
709 | }
710 | }
711 |
--------------------------------------------------------------------------------
/Source/Protocols/OPElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OPElement.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // A protocol that defines all the properties shared by nodes, ways, and relations
12 | public protocol OPElement {
13 | var id: Int { get } // The elements identifier
14 | var tags: [String: String] { get } // Tags that add additional details
15 | var isInteresting: Bool { get } // Does the element have one or more interesting tags?
16 |
17 | // If the element will be rendered as part of a parent element it does not need to be rendered individually
18 | var isSkippable: Bool { get set }
19 |
20 | var geometry: OPGeometry { get } // The element's geometry can take various forms.
21 |
22 | var meta: OPMeta? { get }
23 | }
24 |
--------------------------------------------------------------------------------
/Source/Utilities/CLLocationCoordinate2D+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CLLocationCoordinate2D+Extensions.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import CoreLocation
10 |
11 | // An extension for determining whether to coordinates are equal to one another
12 | extension CLLocationCoordinate2D {
13 |
14 | func isEqual(to coordinate: CLLocationCoordinate2D) -> Bool {
15 | return self.latitude == coordinate.latitude && self.longitude == coordinate.longitude
16 | }
17 | }
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Source/Utilities/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/19/19.
6 | //
7 |
8 | import Foundation
9 |
10 | // Constants for Keys and Values that can potentially arise in Overpass API JSON response data.
11 | struct Overpass {
12 |
13 | struct Keys {
14 | static let name = "name"
15 | static let area = "area"
16 | static let type = "type"
17 | static let latitude = "lat"
18 | static let longitude = "lon"
19 | }
20 |
21 | struct Values {
22 | static let no = "no"
23 | static let outer = "outer"
24 | static let inner = "inner"
25 | static let multipolygon = "multipolygon"
26 | static let barrier = "barrier"
27 | static let route = "route"
28 | static let waterway = "waterway"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Source/Utilities/DecoderExtractor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DecoderExtractor.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/5/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // A dummy class used to extract decoders that are typically only accessable in init functions.
12 | struct DecoderExtractor: Decodable {
13 |
14 | let decoder: Decoder
15 |
16 | init(from decoder: Decoder) throws {
17 | self.decoder = decoder
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Source/Utilities/Errors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Errors.swift
3 | // SwiftOverpassAPI
4 | //
5 | // Created by Edward Samson on 10/6/19.
6 | // Copyright © 2019 Edward Samson. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Errors that can result from Overpass requests
12 | public enum OPRequestError: LocalizedError {
13 | case badResponse(HTTPURLResponse)
14 | case nilData
15 | case decodingFailed
16 | case queryCancelled
17 |
18 | public var errorDescription: String? {
19 | switch self {
20 | case .badResponse(let response):
21 | return "Bad HTTP response: \(response)"
22 | case .nilData:
23 | return "Query response returned nil data"
24 | case .decodingFailed:
25 | return "Query response data could not be decoded"
26 | case .queryCancelled:
27 | return "Query cancelled by user"
28 | }
29 | }
30 | }
31 |
32 | // Erros that can result from decoding overpass elements
33 | public enum OPElementDecoderError: LocalizedError {
34 | case invalidWayLength(wayId: Int)
35 | case unexpectedNil(elementId: Int)
36 | case emptyRelation
37 |
38 | public var errorDescription: String? {
39 | switch self {
40 | case .invalidWayLength(let id):
41 | return "Unable to construct the full geometry for way with id: \(id)"
42 | case .unexpectedNil(let elementId):
43 | return "Unexpected nil when decoding element with id: \(elementId)"
44 | case .emptyRelation:
45 | return "Unable to create geometry for relation with 0 valid members"
46 |
47 | }
48 | }
49 | }
50 |
51 | // Errors that can result from attempting to build invalid Overpass API queries
52 | public enum OPQueryBuilderError: LocalizedError {
53 | case noElementTypesSpecified
54 |
55 | public var errorDescription: String? {
56 | switch self {
57 | case .noElementTypesSpecified:
58 | return "Queries must contain at least one element type"
59 | }
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/Source/Utilities/LinkedList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinkedList.swift
3 | // Pods-SwiftOverpassAPI_Example
4 | //
5 | // Created by Edward Samson on 10/25/19.
6 | //
7 |
8 | import Foundation
9 |
10 | class LinkedListNode {
11 | var value: T
12 | var next: LinkedListNode?
13 |
14 | public init(value: T) {
15 | self.value = value
16 | }
17 | }
18 |
19 | // A singly linked list
20 | class LinkedList {
21 | typealias Node = LinkedListNode
22 |
23 | private var head: Node?
24 | private var tail: Node?
25 |
26 | var isEmpty: Bool {
27 | return head == nil
28 | }
29 |
30 | var first: Node? {
31 | return head
32 | }
33 |
34 | var last: Node? {
35 | return tail
36 | }
37 |
38 | var count = 0
39 |
40 | func append(value: T) {
41 | let newNode = Node(value: value)
42 | if let lastNode = last {
43 | lastNode.next = newNode
44 | } else {
45 | head = newNode
46 | }
47 | tail = newNode
48 | count += 1
49 | }
50 |
51 | func node(atIndex index: Int) -> Node {
52 | if index == 0 {
53 | return head!
54 | } else {
55 | var node = head!.next
56 | for _ in 1.. T {
86 | if index == 0 {
87 | let value = head!.value
88 |
89 | if let next = head?.next {
90 | head = next
91 | count -= 1
92 | } else {
93 | removeAll()
94 | }
95 | return value
96 | }
97 |
98 | let prev = node(atIndex: index - 1)
99 | let value = prev.next!.value
100 |
101 | if let newNext = prev.next?.next {
102 | prev.next = newNext
103 | } else {
104 | tail = prev
105 | }
106 |
107 | count -= 1
108 | return value
109 | }
110 |
111 | func removeAll() {
112 | head = nil
113 | tail = nil
114 | count = 0
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/SwiftOverpassAPI.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint SwiftOverpassAPI.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'SwiftOverpassAPI'
11 | s.version = '0.1.3'
12 | s.summary = 'Query, process, and visualize Overpass API data.'
13 |
14 | # This description is used to generate tags and improve search results.
15 | # * Think: What does it do? Why did you write it? What is the focus?
16 | # * Try to keep it short, snappy and to the point.
17 | # * Write the description between the DESC delimiters below.
18 | # * Finally, don't worry about the indent, CocoaPods strips it!
19 |
20 | s.description = <<-DESC
21 | 'SwiftOverpassAPI is an interface for writing queries to Overpass API, a read only API for OpenStreetMap data. The returned results will be processed and ready for visualization. A simple MapKit visualization struct has been included for those who want to get started quickly'
22 | DESC
23 |
24 | s.homepage = 'https://github.com/ebsamson3/SwiftOverpassAPI'
25 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
26 | s.license = { :type => 'MIT', :file => 'LICENSE' }
27 | s.author = { 'ebsamson3' => 'ebsamson3@gmail.com' }
28 | s.source = { :git => 'https://github.com/ebsamson3/SwiftOverpassAPI.git', :tag => s.version.to_s }
29 | # s.social_media_url = 'https://twitter.com/'
30 |
31 | s.ios.deployment_target = '8.0'
32 | s.swift_version = '5.0'
33 |
34 | s.source_files = 'Source/**/*.swift'
35 |
36 | # s.resource_bundles = {
37 | # 'SwiftOverpassAPI' => ['SwiftOverpassAPI/Assets/*.png']
38 | # }
39 |
40 | # s.public_header_files = 'Pod/Classes/**/*.h'
41 | # s.frameworks = 'UIKit', 'MapKit'
42 | # s.dependency 'AFNetworking', '~> 2.3'
43 | end
44 |
--------------------------------------------------------------------------------
/SwiftOverpassAPI/Assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebsamson3/SwiftOverpassAPI/a96bea6ea42cd54197a9bf989b5ebcf6624403dc/SwiftOverpassAPI/Assets/.gitkeep
--------------------------------------------------------------------------------
/SwiftOverpassAPI/Classes/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ebsamson3/SwiftOverpassAPI/a96bea6ea42cd54197a9bf989b5ebcf6624403dc/SwiftOverpassAPI/Classes/.gitkeep
--------------------------------------------------------------------------------
/_Pods.xcodeproj:
--------------------------------------------------------------------------------
1 | Example/Pods/Pods.xcodeproj
--------------------------------------------------------------------------------