├── .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 | 25 | 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 | [![CI Status](https://img.shields.io/travis/ebsamson3/SwiftOverpassAPI.svg?style=flat)](https://travis-ci.org/ebsamson3/SwiftOverpassAPI) 4 | [![Version](https://img.shields.io/cocoapods/v/SwiftOverpassAPI.svg?style=flat)](https://cocoapods.org/pods/SwiftOverpassAPI) 5 | [![License](https://img.shields.io/cocoapods/l/SwiftOverpassAPI.svg?style=flat)](https://cocoapods.org/pods/SwiftOverpassAPI) 6 | [![Platform](https://img.shields.io/cocoapods/p/SwiftOverpassAPI.svg?style=flat)](https://cocoapods.org/pods/SwiftOverpassAPI) 7 | 8 |

9 | Busch Stadium 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 | Chicago Buildings 313 | Chicago Tourism 314 | Bart Subway Lines 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 --------------------------------------------------------------------------------