From 62bb7e2e2446e44635e8e2b95e3bdc9d52db5a14 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 16 May 2024 14:59:26 +0200 Subject: [PATCH 01/44] Add Example project --- .gitignore | 2 +- Example/Example.xcodeproj/project.pbxproj | 382 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 59 +++ Example/example/AppDelegate.swift | 30 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + Example/example/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 ++ Example/example/Base.lproj/Main.storyboard | 24 ++ Example/example/Info.plist | 31 ++ Example/example/SceneDelegate.swift | 47 +++ Example/example/Terrain.json | 1 + Example/example/Toursprung.swift | 302 ++++++++++++++ Example/example/ViewController.swift | 105 +++++ 16 files changed, 1052 insertions(+), 1 deletion(-) create mode 100644 Example/Example.xcodeproj/project.pbxproj create mode 100644 Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Example/example/AppDelegate.swift create mode 100644 Example/example/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Example/example/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/example/Assets.xcassets/Contents.json create mode 100644 Example/example/Base.lproj/LaunchScreen.storyboard create mode 100644 Example/example/Base.lproj/Main.storyboard create mode 100644 Example/example/Info.plist create mode 100644 Example/example/SceneDelegate.swift create mode 100644 Example/example/Terrain.json create mode 100644 Example/example/Toursprung.swift create mode 100644 Example/example/ViewController.swift diff --git a/.gitignore b/.gitignore index 72dde1df7..dc1234b85 100644 --- a/.gitignore +++ b/.gitignore @@ -129,7 +129,7 @@ iOSInjectionProject/ Packages xcuserdata *.xcodeproj - +!Example/Example.xcodeproj ### SwiftPM ### diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj new file mode 100644 index 000000000..7e605e864 --- /dev/null +++ b/Example/Example.xcodeproj/project.pbxproj @@ -0,0 +1,382 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + CD958B4B2BF6156100501F93 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4A2BF6156100501F93 /* AppDelegate.swift */; }; + CD958B4D2BF6156100501F93 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4C2BF6156100501F93 /* SceneDelegate.swift */; }; + CD958B4F2BF6156100501F93 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4E2BF6156100501F93 /* ViewController.swift */; }; + CD958B522BF6156100501F93 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CD958B512BF6156100501F93 /* Base */; }; + CD958B542BF6156200501F93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD958B532BF6156200501F93 /* Assets.xcassets */; }; + CD958B572BF6156200501F93 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CD958B562BF6156200501F93 /* Base */; }; + CD958B622BF615D200501F93 /* Terrain.json in Resources */ = {isa = PBXBuildFile; fileRef = CD958B612BF615D200501F93 /* Terrain.json */; }; + CD958B642BF6184900501F93 /* Toursprung.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B632BF6184900501F93 /* Toursprung.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + CD958B472BF6156100501F93 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CD958B4A2BF6156100501F93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + CD958B4C2BF6156100501F93 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + CD958B4E2BF6156100501F93 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + CD958B512BF6156100501F93 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + CD958B532BF6156200501F93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CD958B562BF6156200501F93 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + CD958B582BF6156200501F93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CD958B612BF615D200501F93 /* Terrain.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Terrain.json; sourceTree = ""; }; + CD958B632BF6184900501F93 /* Toursprung.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toursprung.swift; sourceTree = ""; }; + CD958B652BF63A6700501F93 /* maplibre-navigation-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "maplibre-navigation-ios"; path = ..; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CD958B442BF6156100501F93 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CD958B3E2BF6156100501F93 = { + isa = PBXGroup; + children = ( + CD958B652BF63A6700501F93 /* maplibre-navigation-ios */, + CD958B492BF6156100501F93 /* example */, + CD958B482BF6156100501F93 /* Products */, + ); + sourceTree = ""; + }; + CD958B482BF6156100501F93 /* Products */ = { + isa = PBXGroup; + children = ( + CD958B472BF6156100501F93 /* Example.app */, + ); + name = Products; + sourceTree = ""; + }; + CD958B492BF6156100501F93 /* example */ = { + isa = PBXGroup; + children = ( + CD958B4A2BF6156100501F93 /* AppDelegate.swift */, + CD958B4C2BF6156100501F93 /* SceneDelegate.swift */, + CD958B4E2BF6156100501F93 /* ViewController.swift */, + CD958B632BF6184900501F93 /* Toursprung.swift */, + CD958B612BF615D200501F93 /* Terrain.json */, + CD958B502BF6156100501F93 /* Main.storyboard */, + CD958B532BF6156200501F93 /* Assets.xcassets */, + CD958B552BF6156200501F93 /* LaunchScreen.storyboard */, + CD958B582BF6156200501F93 /* Info.plist */, + ); + path = example; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CD958B462BF6156100501F93 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = CD958B5B2BF6156200501F93 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + CD958B432BF6156100501F93 /* Sources */, + CD958B442BF6156100501F93 /* Frameworks */, + CD958B452BF6156100501F93 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + packageProductDependencies = ( + ); + productName = "navigation sample"; + productReference = CD958B472BF6156100501F93 /* Example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CD958B3F2BF6156100501F93 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + CD958B462BF6156100501F93 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = CD958B422BF6156100501F93 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CD958B3E2BF6156100501F93; + packageReferences = ( + ); + productRefGroup = CD958B482BF6156100501F93 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CD958B462BF6156100501F93 /* Example */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CD958B452BF6156100501F93 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD958B542BF6156200501F93 /* Assets.xcassets in Resources */, + CD958B622BF615D200501F93 /* Terrain.json in Resources */, + CD958B572BF6156200501F93 /* Base in Resources */, + CD958B522BF6156100501F93 /* Base in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CD958B432BF6156100501F93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD958B642BF6184900501F93 /* Toursprung.swift in Sources */, + CD958B4F2BF6156100501F93 /* ViewController.swift in Sources */, + CD958B4B2BF6156100501F93 /* AppDelegate.swift in Sources */, + CD958B4D2BF6156100501F93 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + CD958B502BF6156100501F93 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + CD958B512BF6156100501F93 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + CD958B552BF6156200501F93 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + CD958B562BF6156200501F93 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + CD958B592BF6156200501F93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + CD958B5A2BF6156200501F93 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + CD958B5C2BF6156200501F93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = example/Info.plist; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "for live navigation"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CD958B5D2BF6156200501F93 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = example/Info.plist; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "for live navigation"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CD958B422BF6156100501F93 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD958B592BF6156200501F93 /* Debug */, + CD958B5A2BF6156200501F93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CD958B5B2BF6156200501F93 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD958B5C2BF6156200501F93 /* Debug */, + CD958B5D2BF6156200501F93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CD958B3F2BF6156100501F93 /* Project object */; +} diff --git a/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..5635fc2c8 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,59 @@ +{ + "pins" : [ + { + "identity" : "mapbox-directions-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/flitsmeister/mapbox-directions-swift", + "state" : { + "revision" : "6c19ecc4e1324887ae3250802b8d13d8d8b3ff2d", + "version" : "0.23.3" + } + }, + { + "identity" : "maplibre-gl-native-distribution", + "kind" : "remoteSourceControl", + "location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git", + "state" : { + "revision" : "6579f48fe126ce2916f7b5d0c84c1869d790c4e4", + "version" : "6.4.1" + } + }, + { + "identity" : "polyline", + "kind" : "remoteSourceControl", + "location" : "https://github.com/raphaelmor/Polyline", + "state" : { + "revision" : "353f80378dcd8f17eefe8550090c6b1ae3c9da23", + "version" : "5.1.0" + } + }, + { + "identity" : "solar", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ceeK/Solar.git", + "state" : { + "revision" : "c2b96f2d5fb7f835b91cefac5e83101f54643901", + "version" : "3.0.1" + } + }, + { + "identity" : "swiftformat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/SwiftFormat.git", + "state" : { + "revision" : "05cb325003a673b3d177c711b3bc909cfee07622", + "version" : "0.53.9" + } + }, + { + "identity" : "turf-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/flitsmeister/turf-swift", + "state" : { + "revision" : "b05b4658d1b48eac4127a0d9ebbb5a6f965a8251", + "version" : "0.2.2" + } + } + ], + "version" : 2 +} diff --git a/Example/example/AppDelegate.swift b/Example/example/AppDelegate.swift new file mode 100644 index 000000000..4362a5696 --- /dev/null +++ b/Example/example/AppDelegate.swift @@ -0,0 +1,30 @@ +// +// AppDelegate.swift +// Example +// +// Created by Patrick Kladek on 16.05.24. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/Example/example/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/example/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Example/example/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/Example/example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/example/Assets.xcassets/Contents.json b/Example/example/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Example/example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/example/Base.lproj/LaunchScreen.storyboard b/Example/example/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..865e9329f --- /dev/null +++ b/Example/example/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/example/Base.lproj/Main.storyboard b/Example/example/Base.lproj/Main.storyboard new file mode 100644 index 000000000..25a763858 --- /dev/null +++ b/Example/example/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/example/Info.plist b/Example/example/Info.plist new file mode 100644 index 000000000..051525654 --- /dev/null +++ b/Example/example/Info.plist @@ -0,0 +1,31 @@ + + + + + MGLMapboxAccessToken + empty + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIBackgroundModes + + audio + + + diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift new file mode 100644 index 000000000..b3e21d8bf --- /dev/null +++ b/Example/example/SceneDelegate.swift @@ -0,0 +1,47 @@ +// +// SceneDelegate.swift +// Example +// +// Created by Patrick Kladek on 16.05.24. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/Example/example/Terrain.json b/Example/example/Terrain.json new file mode 100644 index 000000000..0e4e75cd6 --- /dev/null +++ b/Example/example/Terrain.json @@ -0,0 +1 @@ +{"version":8,"name":"Terrain","metadata":{"mtk:formatStyle":true,"mtk:preload_source":{"tileSize":256,"tiles":["https://vtc-cdn.maptoolkit.net/rtc/toursprung-terrain/{z}/{x}/{y}.png?api_key="],"type":"raster"},"mtk:user":"hudhud","rapidapi":true},"light":{"anchor":"viewport","color":"white","intensity":0.1},"center":[13.255,47.723],"zoom":7,"sprite":"https://static.maptoolkit.net/sprites/hudhud","glyphs":"https://static.maptoolkit.net/fonts/{fontstack}/{range}.pbf","sources":{"hillshading":{"type":"raster","url":"https://vtc-cdn.maptoolkit.net/hillshading.json?api_key="},"mtk":{"type":"vector","url":"https://vtc-cdn.maptoolkit.net/mtk-contours-bathymetry.json?api_key="},"naturalearth":{"type":"raster","url":"https://vdata.maptoolkit.net/toursprung/naturalearth.json"}},"layers":[{"id":"background_","type":"background","minzoom":0,"maxzoom":22,"layout":{"visibility":"visible"},"paint":{"background-color":{"base":1,"stops":[[11,"#f7f9ee"],[14,"#fefefa"]]}}},{"id":"naturalearth","type":"raster","source":"naturalearth","minzoom":0,"maxzoom":7,"layout":{"visibility":"visible"},"paint":{"raster-opacity":{"base":1.5,"stops":[[5,0.6],[7,0.1]]}}},{"id":"greenland","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":5,"maxzoom":10,"filter":["all",["in","type","wood","farmland","grass"]],"layout":{"visibility":"visible"},"paint":{"fill-antialias":false,"fill-color":"#a3cf61","fill-opacity":{"base":1.2,"stops":[[6,0.15],[9,0.3],[10,0.1]]}}},{"id":"landuse_builtup_residential","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Buildings"},"minzoom":5,"maxzoom":22,"filter":["==","type","residential"],"layout":{"visibility":"visible"},"paint":{"fill-color":{"stops":[[12.9,"#e8e8e8"],[13,"#F4F4F4"]]},"fill-opacity":{"base":1.4,"stops":[[5,0.3],[9,0.8]]}}},{"id":"natural_wetland","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":8,"maxzoom":22,"filter":["==","type","wetland"],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[10,0.9],[11,1]]},"fill-pattern":"wetland"}},{"id":"landuse_park","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Park"},"minzoom":5,"maxzoom":22,"filter":["in","type","park"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#c7e78d","fill-opacity":0.9}},{"id":"landuse_cemetery","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Landuse"},"minzoom":9,"maxzoom":22,"filter":["==","type","cemetery"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#cff092"}},{"id":"landuse_hospital","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Landuse"},"minzoom":11,"maxzoom":22,"filter":["in","type","hospital","college"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#e8e3d9"}},{"id":"landuse_school","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Landuse"},"minzoom":14,"maxzoom":22,"filter":["==","type","school"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#e8e3d9"}},{"id":"natural_wood","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Wood"},"minzoom":9,"maxzoom":22,"filter":["all",["==","type","wood"]],"layout":{"visibility":"visible"},"paint":{"fill-antialias":false,"fill-color":"#a3cf61","fill-opacity":0.6}},{"id":"natural_grass","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Grass"},"minzoom":9,"maxzoom":22,"filter":["all",["in","type","grass","farmland"],["!=","subtype","park"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#ebf9c2","fill-opacity":0.7}},{"id":"poi_label_garden","type":"fill","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Landuse"},"minzoom":11,"maxzoom":22,"filter":["==","type","garden"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#ddecb1","fill-opacity":1}},{"id":"natural_glacier","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":9,"maxzoom":22,"filter":["==","subtype","glacier"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#e1ecf2","fill-opacity":{"base":1,"stops":[[9,0.1],[10,1]]}}},{"id":"natural_sand","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":5,"maxzoom":22,"filter":["==","type","sand"],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(60, 46%, 87%)"}},{"id":"natural_pitch","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["==","subtype","pitch"],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(100, 57%, 72%)","fill-opacity":0.2}},{"id":"poi_label_pitch","type":"fill","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["in","type","stadium","sports_centre","athletics"],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(100, 57%, 72%)","fill-opacity":0.2}},{"id":"natural_bare_rock","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["==","subtype","bare_rock"],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[15,0.6],[17,0.3]]},"fill-pattern":"bare_rock"}},{"id":"natural_bare_scree","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":11,"maxzoom":22,"filter":["in","subtype","bare_scree","scree"],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[13,0.2],[14,0.3],[15,0.3],[17,0.2]]},"fill-pattern":"scree"}},{"id":"natural_nationalpark","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":6,"maxzoom":22,"filter":["in","subtype","protect_class_2"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#8bb153","fill-opacity":{"base":0.3,"stops":[[8,0.5],[11,0.6],[12,0.2]]}}},{"id":"natural_protected_area","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":11,"maxzoom":22,"filter":["all",["in","type","nature_reserve","protected_area"],["!=","subtype","protect_class_2"],["!=","seasonal","winter"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#8bb153","fill-opacity":{"base":0.3,"stops":[[8,0.5],[11,0.6],[12,0.2]]}}},{"id":"natural_line_protected_area","type":"line","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":12,"maxzoom":22,"filter":["all",["in","type","nature_reserve","protected_area"],["!=","subtype","protect_class_2"],["!=","seasonal","winter"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#84A94C","line-opacity":{"base":0.4,"stops":[[10,0.2],[12,0.6]]},"line-width":{"base":1,"stops":[[12,2],[14,4]]}}},{"id":"natural_line_nationalpark","type":"line","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":6,"maxzoom":22,"filter":["in","subtype","protect_class_2"],"layout":{"visibility":"visible"},"paint":{"line-color":"#84A94C","line-opacity":{"base":0.4,"stops":[[10,0.2],[12,1]]},"line-width":{"base":1,"stops":[[12,4],[14,6]]}}},{"id":"contours_10","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":15,"maxzoom":22,"filter":["==","divisor",10],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"stops":[[15,0.4],[16,0.6]],"base":1}}},{"id":"contours_20","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":5,"maxzoom":22,"filter":["==","divisor",20],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.2],[12,0.4],[14,0.6],[16,0.8]]}}},{"id":"contours_50","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":5,"maxzoom":22,"filter":["==","divisor",50],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.4],[12,0.6],[14,0.8],[16,1]]}}},{"id":"contours_100","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":11,"maxzoom":22,"filter":["==","divisor",100],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.6],[12,0.8],[14,1],[16,1.2]]}}},{"id":"contours_500","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":11,"maxzoom":22,"filter":["all",["==","divisor",500],["!=","ele",0]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.8],[12,1],[14,1.2],[16,1.5]]}}},{"id":"hillshading","type":"raster","source":"hillshading","metadata":{"maptoolkit:category":"Hillshading"},"minzoom":5,"maxzoom":22,"layout":{"visibility":"visible"},"paint":{"raster-opacity":{"stops":[[6,0.25],[12,0.6],[15,0.4],[17,0.2]]}}},{"id":"natural_cliff","type":"line","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["==","type","cliff"],"layout":{"visibility":"visible"},"paint":{"line-opacity":{"base":1,"stops":[[13,0.5],[15,1]]},"line-pattern":"cliff","line-width":8}},{"id":"water_other","type":"line","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["all",["==","$type","LineString"],["!=","type","river"],["!=","type","stream"],["!=","type","canal"]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#b7d6ff","line-width":{"base":1.3,"stops":[[13,0.5],[20,2]]}}},{"id":"water_river","type":"line","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","river"]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#b7d6ff","line-width":{"base":1.2,"stops":[[11,0.5],[20,6]]}}},{"id":"water_stream","type":"line","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["all",["==","$type","LineString"],["in","type","stream","canal"]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#b7d6ff","line-width":{"base":1.3,"stops":[[13,0.5],[20,6]]}}},{"id":"water","type":"fill","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#ADD1FF"}},{"id":"bathymetry","type":"fill","source":"mtk","source-layer":"bathymetry","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#61a6ff","fill-opacity":0.12}},{"id":"landuse_pedestrian","type":"fill","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Pedestrian"},"minzoom":12,"maxzoom":22,"filter":["all",["==","$type","Polygon"],["==","subtype","pedestrian"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#dce2e5","fill-opacity":{"base":1,"stops":[[12,0.3],[15,0.9]]}}},{"id":"landuse_aeroway_fill","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Airport"},"minzoom":11,"maxzoom":22,"filter":["all",["==","type","aeroway"],["==","$type","Polygon"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#f0ede9","fill-opacity":0.6}},{"id":"aeroway_runway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Airport"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","subtype","runway"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#f0ede9","line-width":{"base":1.2,"stops":[[11,3],[20,16]]}}},{"id":"road_aeroway_taxiway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Airport"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","subtype","taxiway"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#f0ede9","line-width":{"base":1.2,"stops":[[11,0.5],[20,6]]}}},{"id":"road_tunnel_motorway_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"],["==","ramp",1]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-dasharray":[0.5,0.25],"line-opacity":1,"line-width":{"base":1.2,"stops":[[12,1],[13,3],[14,4],[20,15]]}}},{"id":"road_tunnel_service_track_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","service","track"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#cfcdca","line-dasharray":[0.5,0.25],"line-width":{"base":1.2,"stops":[[15,1],[16,4],[20,11]]}}},{"id":"road_tunnel_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","ramp",1],["!=","indoor",1],["!=","type","path"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-opacity":1,"line-width":{"base":1.2,"stops":[[12,1],[13,3],[14,4],[20,15]]}}},{"id":"road_tunnel_minor_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","structure","tunnel"],["==","type","minor"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#cfcdca","line-opacity":{"stops":[[12,0],[12.5,1]]},"line-width":{"base":1.2,"stops":[[12,0.5],[13,1],[14,4],[20,15]]}}},{"id":"road_tunnel_secondary_tertiary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","secondary","tertiary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tunnel_trunk_primary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","trunk","primary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tunnel_motorway_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-dasharray":[0.5,0.25],"line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tunnel_path","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","path"]],"layout":{"visibility":"none"},"paint":{"line-color":"#cba","line-dasharray":[1.5,0.75],"line-width":{"base":1.2,"stops":[[15,1.2],[20,4]]}}},{"id":"road_tunnel_motorway_ramp","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"],["==","ramp",1]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.2,"stops":[[12.5,0],[13,1.5],[14,2.5],[20,11.5]]}}},{"id":"road_tunnel_service_track","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","service","track"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.2,"stops":[[15.5,0],[16,2],[20,7.5]]}}},{"id":"road_tunnel_link","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","ramp",1],["!=","indoor",1],["!=","type","path"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff4c6","line-width":{"base":1.2,"stops":[[12.5,0],[13,1.5],[14,2.5],[20,11.5]]}}},{"id":"road_tunnel_minor","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","structure","tunnel"],["==","type","minor"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":1,"line-width":{"base":1.2,"stops":[[13.5,0],[14,2.5],[20,11.5]]}}},{"id":"road_tunnel_secondary_tertiary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","secondary","tertiary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff4c6","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tunnel_trunk_primary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","trunk","primary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff4c6","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tunnel_motorway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffdaa6","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tunnel_rail","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-opacity":0.6,"line-width":{"base":1.4,"stops":[[14,0.4],[15,0.75],[20,2]]}}},{"id":"road_tunnel_rail_hatching","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-dasharray":[0.2,8],"line-opacity":0.6,"line-width":{"base":1.4,"stops":[[14.5,0],[15,3],[20,8]]}}},{"id":"road_motorway_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":7,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["!in","structure","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_track_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","track"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#a6a6a6","line-dasharray":[1,2],"line-opacity":0.9,"line-width":{"base":1.4,"stops":[[12,1],[16,4],[20,11]]}}},{"id":"road_service_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","service"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#cfcdca","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[12,0.75],[20,2]]}}},{"id":"road_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":13,"maxzoom":22,"filter":["all",["==","ramp",1],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"none"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_minor_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","minor"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[11,0.75],[20,2]]}}},{"id":"road_secondary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","secondary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[9,0.1],[10,1]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tertiary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","tertiary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[9,0.1],[10,1]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_trunk_primary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":10,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","trunk","primary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[7,0.05],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[7,0.1],[10,1]]},"line-width":{"stops":[[10,0.75],[18,2]],"base":1.2}}},{"id":"road_motorway_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","motorway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"stops":[[10,0.75],[18,2]],"base":1.2}}},{"id":"road_path_pedestrian","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Pedestrian","mtk:states":{"hiking":{"minzoom":13,"paint":{"line-width":2}}}},"minzoom":15,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","path"],["!in","brunnel","bridge","tunnel"],["!in","subtype","platform"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#c3c3c3","line-dasharray":[2,1.5],"line-opacity":0.9,"line-width":{"base":1.2,"stops":[[15,1.5],[20,2.5]]}}},{"id":"road_via_ferrata","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Pedestrian","mtk:states":{"hiking":{"minzoom":13,"paint":{"line-width":2}}}},"minzoom":15,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","via_ferrata"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-dasharray":[0.75,1.8],"line-opacity":0.8,"line-width":{"base":1.2,"stops":[[15,1.2],[20,2.5]]}}},{"id":"road_motorway_ramp","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":7,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["!in","structure","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_track","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","track"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":0.9,"line-width":{"base":1.2,"stops":[[12,0],[16,2],[20,7.5]]}}},{"id":"road_service","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","service"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_ramp","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":13,"maxzoom":22,"filter":["all",["==","ramp",1],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"none"},"paint":{"line-color":"#fff","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_minor","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","minor"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":1,"line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_secondary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","secondary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[9,"#fff"],[22,"#fff"]]},"line-opacity":{"stops":[[8,0.1],[10,1]]},"line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tertiary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","tertiary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":{"stops":[[8,0.1],[10,1]]},"line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_trunk_primary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":7,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","trunk","primary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[7.9,"#fff"],[8,"#fffd8b"],[11,"#fffd00"],[22,"#fffd00"]]},"line-opacity":{"stops":[[7,0.8],[10,1]]},"line-width":{"base":1.5,"stops":[[7,1],[10,2],[18,26]]}}},{"id":"road_motorway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":6,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","motorway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[6.9,"#fff"],[7,"#ffc345"]]},"line-width":{"base":1.5,"stops":[[6,0.3],[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_rail","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","railway"],["in","subtype","rail","light_rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-width":{"base":1.4,"stops":[[14,0.4],[15,0.75],[20,2]]}}},{"id":"road_rail_hatching","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","railway"],["in","subtype","rail","light_rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-dasharray":[0.2,8],"line-width":{"base":1.4,"stops":[[14.5,0],[15,3],[20,8]]}}},{"id":"road_bridge_motorway_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_service_track_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","service","track"],["==","brunnel","bridge"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[12,0.75],[20,2]]}}},{"id":"road_bridge_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-opacity":1,"line-width":{"base":1.4,"stops":[[10,1.25],[12,1.25],[18,16]]}}},{"id":"road_bridge_minor_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","minor"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[12,0.75],[20,2]]}}},{"id":"road_bridge_secondary_tertiary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","secondary","tertiary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[9,0.1],[10,1]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_trunk_primary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","trunk","primary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_motorway_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_path","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":16.5,"maxzoom":22,"filter":["all",["==","brunnel","bridge"],["==","type","path"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#cba","line-dasharray":[1.5,0.75],"line-width":{"base":1.2,"stops":[[15,1.2],[20,4]]}}},{"id":"road_bridge_motorway_link","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_service_track","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","service","track"],["==","brunnel","bridge"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_bridge_link","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fffd8b","line-width":{"base":1.2,"stops":[[12.5,0],[13,1.5],[14,2.5],[20,11.5]]}}},{"id":"road_bridge_minor","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","minor"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":1,"line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_bridge_secondary_tertiary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","secondary","tertiary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[8,"#666"],[9,"#fff"],[22,"#fff"]]},"line-opacity":{"stops":[[8,0.1],[10,1]]},"line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_trunk_primary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","trunk","primary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fffd8b","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_motorway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_rail","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","rail"],["==","brunnel","bridge"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-width":{"base":1.4,"stops":[[14,0.4],[15,0.75],[20,2]]}}},{"id":"road_bridge_rail_hatching","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","rail"],["==","brunnel","bridge"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-dasharray":[0.2,8],"line-width":{"base":1.4,"stops":[[14.5,0],[15,3],[20,8]]}}},{"id":"road_walking_local","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Hiking","mtk:states":{"hiking":{"minzoom":13,"paint":{"line-opacity":0.7}}}},"minzoom":14,"maxzoom":22,"filter":["all",["==","network","lwn"],["!=","type","via_ferrata"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-dasharray":[1.5,0.75],"line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_walking_regional","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Hiking","mtk:states":{"hiking":{"minzoom":11,"paint":{"line-opacity":0.7}}}},"minzoom":13,"maxzoom":22,"filter":["==","network","rwn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-dasharray":[1.5,0.75],"line-opacity":0.5,"line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_hiking_international","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Hiking","mtk:states":{"hiking":{"minzoom":11,"paint":{"line-opacity":0.7}}}},"minzoom":12,"maxzoom":22,"filter":["in","network","iwn","nwn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-opacity":0.5,"line-width":{"base":1.2,"stops":[[8,1],[20,4]]}}},{"id":"road_cycling_local","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Cycling","mtk:states":{"cycling":{"minzoom":12,"paint":{"line-opacity":0.9,"line-width":{"base":1.2,"stops":[[12,2],[20,3]]}}}}},"minzoom":14,"maxzoom":22,"filter":["in","network","lcn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#6dad3e","line-dasharray":[1.5,0.75],"line-opacity":0.7,"line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_cycling_regional","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Cycling","mtk:states":{"cycling":{"minzoom":11,"paint":{"line-opacity":0.8}}}},"minzoom":13,"maxzoom":22,"filter":["in","network","rcn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#6dad3e","line-opacity":0.7,"line-width":{"base":1.2,"stops":[[8,1],[20,4]]}}},{"id":"road_cycling_international","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Cycling","mtk:states":{"cycling":{"minzoom":10,"paint":{"line-opacity":0.8,"line-width":{"base":1.2,"stops":[[8,2],[20,4]]}}}}},"minzoom":11,"maxzoom":22,"filter":["in","network","icn","ncn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#6dad3e","line-opacity":0.7,"line-width":{"base":1.2,"stops":[[8,1],[20,4]]}}},{"id":"road_transit_tram","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Public Transport"},"minzoom":16,"maxzoom":22,"filter":["all",["==","type","transit"],["==","subtype","tram"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#acacac","line-opacity":0.7,"line-width":{"base":1.2,"stops":[[16,1],[20,4]]}}},{"id":"road_ferry","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Ferry"},"minzoom":11,"maxzoom":22,"filter":["in","type","ferry"],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-dasharray":[1,2],"line-width":{"base":1.2,"stops":[[10,0.3],[14,1]]}}},{"id":"admin_level_3","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",["in","admin_level",3,4],["==","maritime",0]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#9e9cab","line-dasharray":[3,1,1,1],"line-opacity":0.8,"line-width":{"base":1,"stops":[[4,0.4],[5,1],[12,3]]}}},{"id":"admin_level_2","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":0,"maxzoom":22,"filter":["all",["==","admin_level",2],["==","disputed",0],["==","maritime",0]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(230, 8%, 51%)","line-width":{"base":1,"stops":[[3,0.5],[10,2]]}}},{"id":"admin_level_2_disputed","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",["==","admin_level",2],["==","disputed",1],["==","maritime",0]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#9e9cab","line-dasharray":[2,2],"line-width":{"base":1,"stops":[[4,1.4],[5,2],[12,8]]}}},{"id":"admin_level_3_maritime","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",[">=","admin_level",3],["==","maritime",1]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#a0c8f0","line-dasharray":[3,1,1,1],"line-opacity":0.5,"line-width":{"base":1,"stops":[[4,0.4],[5,1],[12,3]]}}},{"id":"admin_level_2_maritime","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",["==","admin_level",2],["==","maritime",1]],"layout":{"line-cap":"round","visibility":"none"},"paint":{"line-color":"#a0c8f0","line-opacity":0.5,"line-width":{"base":1,"stops":[[4,1.4],[5,2],[12,8]]}}},{"id":"road_aerialway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":10,"maxzoom":22,"filter":["all",["==","type","aerialway"],["!in","subtype","t-bar","drag_lift","magic_carpet","rope_tow"]],"layout":{"line-cap":"square","line-join":"round","visibility":"visible"},"paint":{"line-color":"#666","line-opacity":{"stops":[[11,0.35],[14,0.5]]},"line-width":{"stops":[[10,0.5],[12,1.4]]}}},{"id":"road_aerialway_patterns","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":10,"maxzoom":22,"filter":["all",["==","type","aerialway"],["!in","subtype","t-bar","drag_lift","magic_carpet","rope_tow"]],"layout":{"line-cap":"square","line-join":"round","visibility":"visible"},"paint":{"line-color":"#666","line-dasharray":[1,6],"line-opacity":{"stops":[[11,0.35],[14,0.5]]},"line-width":{"stops":[[10,0.5],[12,2.8]]}}},{"id":"road_piste_easy_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-easy"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#0969B3","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,2],[13,5],[14,7],[16,9]]}}},{"id":"road_piste_easy","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-easy"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#0969B3","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_pistes_intermediate_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-intermediate"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#EF2021","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,2],[13,5],[14,7],[16,9]]}}},{"id":"road_pistes_intermediate","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-intermediate"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#EF2021","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_pistes_difficult_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-advanced"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#000","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,2],[13,5],[14,7],[16,9]]}}},{"id":"road_pistes_difficult","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-advanced"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#000","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_pistes_nordic","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","nordic"],["==","subtype","nordic-easy"],["==","subtype","nordic-intermediate"],["==","subtype","nordic-advaned"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#606060","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1.2],[15,3],[22,2]]}}},{"id":"road_pistes_skiroutes","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-freeride"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#EF2021","line-dasharray":[3,2],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1.2],[15,3],[22,2]]}}},{"id":"road_piste_connection_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","connection"],["==","subtype","connection-easy"],["==","subtype","connection-intermediate"],["==","subtype","connection-advaned"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#0969B3","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,4],[13,7],[14,9],[22,9]]}}},{"id":"road_piste_connection","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","connection"],["==","subtype","connection-easy"],["==","subtype","connection-intermediate"],["==","subtype","connection-advaned"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#0969B3","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1.2],[15,3],[22,2]]}}},{"id":"building","type":"fill","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings"},"minzoom":13,"maxzoom":22,"filter":["any",["!has","level"],[">=","level",0]],"layout":{"visibility":"visible"},"paint":{"fill-color":{"stops":[[13,"#E8E8E8"],[15,"#E8E8E8"]]},"fill-opacity":0.5,"fill-outline-color":"#c5c5c5"}},{"id":"building_public","type":"fill","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings"},"minzoom":13,"maxzoom":22,"filter":["all",["any",["!has","level"],[">=","level",0]],["any",["in","building","church","school","public","train_station"],["in","type","library"]]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#F9E7E7","fill-opacity":0.5,"fill-outline-color":"#efbebe"}},{"id":"building_3D","type":"fill-extrusion","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings","mtk:states":{"buildings3d":{"layout":{"visibility":"none"}}}},"minzoom":15,"maxzoom":22,"filter":["==","extrude",true],"layout":{"visibility":"visible"},"paint":{"fill-extrusion-base":["interpolate",["linear"],["zoom"],15,0,15.05,["get","min_height"]],"fill-extrusion-color":"#E8E8E8","fill-extrusion-height":["interpolate",["linear"],["zoom"],15,0,15.05,["get","height"]],"fill-extrusion-opacity":0.6}},{"id":"building_3D_public","type":"fill-extrusion","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings","mtk:states":{"buildings3d":{"layout":{"visibility":"none"}}}},"minzoom":15,"maxzoom":22,"filter":["all",["==","extrude",true],["any",["in","building","church","school","public","train_station"],["in","type","library"]]],"layout":{"visibility":"visible"},"paint":{"fill-extrusion-base":["interpolate",["linear"],["zoom"],15,0,15.05,["get","min_height"]],"fill-extrusion-color":"#efbebe","fill-extrusion-height":["interpolate",["linear"],["zoom"],15,0,15.05,["get","height"]],"fill-extrusion-opacity":0.6}},{"id":"road_oneway_arrows","type":"symbol","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Oneway Arrows"},"minzoom":15,"maxzoom":22,"filter":["all",["==","oneway",1],["!=","type","transit"],["!in","cycleway","opposite","opposite_lane"]],"layout":{"icon-image":{"base":1,"stops":[[15,"oneway"]]},"icon-padding":2,"icon-rotation-alignment":"map","icon-size":0.7,"symbol-placement":"line","symbol-spacing":250,"visibility":"visible"},"paint":{"text-color":"#334"}},{"id":"road_oneway_arrows_except_cycles","type":"symbol","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Oneway Arrows"},"minzoom":15,"maxzoom":22,"filter":["all",["==","oneway",1],["!=","type","transit"],["in","cycleway","opposite","opposite_lane"]],"layout":{"icon-image":{"base":1,"stops":[[15,"oneway-except-cycles"]]},"icon-padding":2,"icon-rotation-alignment":"map","icon-size":0.7,"symbol-placement":"line","symbol-spacing":250,"visibility":"visible"},"paint":{"text-color":"#334"}},{"id":"housenum_label","type":"symbol","source":"mtk","source-layer":"housenum_label","metadata":{"maptoolkit:category":"House Numbers","maptoolkit:type":"label"},"minzoom":17,"maxzoom":22,"layout":{"text-field":"{housenumber}","text-font":["Noto Sans Regular"],"text-max-width":7,"text-pitch-alignment":"viewport","text-size":9.5,"visibility":"visible"},"paint":{"text-color":"hsl(35, 2%, 69%)","text-halo-color":"hsl(35, 8%, 85%)","text-halo-width":0.5,"text-opacity":{"base":1,"stops":[[16,0],[16.5,1]]}}},{"id":"contours_100_label","type":"symbol","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":14,"maxzoom":22,"filter":["==","divisor",100],"layout":{"symbol-placement":"line","symbol-spacing":200,"text-field":"{ele}","text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":8,"visibility":"visible"},"paint":{"text-color":"#e1c26e"}},{"id":"contours_500_label","type":"symbol","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":13,"maxzoom":22,"filter":["all",["==","divisor",500],["!=","ele",0]],"layout":{"symbol-placement":"line","symbol-spacing":200,"text-field":"{ele}","text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":10,"visibility":"visible"},"paint":{"text-color":"#e1c26e"}},{"id":"poi_label_3","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":17,"maxzoom":22,"filter":["all",["!in","subtype","subway_entrance","board","map","guidepost","drag_lift","station","pitch"],["!in","type","information"]],"layout":{"icon-image":"{type}-11","symbol-avoid-edges":false,"text-pitch-alignment":"viewport","visibility":"visible"},"paint":{"icon-opacity":0.4}},{"id":"poi_label_2","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":15,"maxzoom":22,"filter":["all",["<=","rank",50],["!in","subtype","subway_entrance","board","map","guidepost","drag_lift","station","pitch"],["!in","type","information"]],"layout":{"icon-image":"{type}-11","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":2,"text-pitch-alignment":"viewport","text-size":12,"visibility":"visible"},"paint":{"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"poi_label_1","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["all",["<=","rank",8],["!in","subtype","subway_entrance","board","map","guidepost","drag_lift","station","pitch","dog_park","playground","garden","picnic_site"],["!in","type","information"]],"layout":{"icon-image":"{type}-11","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":{"base":1,"stops":[[14,20],[20,2]]},"text-pitch-alignment":"viewport","text-size":10,"visibility":"visible"},"paint":{"icon-opacity":0.6,"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"hsla(0, 0%, 100%, 0.8)","text-halo-width":0.9}},{"id":"poi_toilet","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":17,"maxzoom":22,"filter":["==","type","toilets"],"layout":{"icon-image":"toilet","symbol-avoid-edges":false,"visibility":"visible"},"paint":{"icon-opacity":0.8}},{"id":"poi_generator","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":13,"maxzoom":22,"filter":["==","subtype","wind"],"layout":{"icon-image":"generator_wind","icon-size":1,"symbol-avoid-edges":false,"visibility":"visible"},"paint":{"icon-opacity":0.6}},{"id":"poi_label_bank","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["==","type","bank"],"layout":{"icon-image":"{type}","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":{"base":1,"stops":[[14,20],[20,2]]},"text-pitch-alignment":"viewport","text-size":10,"visibility":"visible"},"paint":{"icon-opacity":0.6,"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"poi_label_rail_station","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":15,"maxzoom":22,"filter":["all",["==","type","station"],["!in","subtype","t-bar","magic_carpet","rope_tow","drag_lift","gondola","cable_car","chair_lift"]],"layout":{"icon-image":"{maki}-11","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":{"base":1,"stops":[[14,20],[20,2]]},"text-pitch-alignment":"viewport","text-size":12,"visibility":"visible"},"paint":{"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"poi_label_aerialway_station_icon","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":10,"maxzoom":22,"filter":["all",["==","type","station"],["in","subtype","gondola","cable_car","chair_lift"],["!in","subtype","t-bar","magic_carpet","rope_tow","drag_lift"]],"layout":{"icon-image":"{subtype}","symbol-avoid-edges":false,"text-pitch-alignment":"viewport","visibility":"visible"}},{"id":"road_label_walking_shied","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Hiking"},"minzoom":12,"maxzoom":22,"filter":["all",["in","network","iwn"],["has","ref"]],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","icon-size":0.7,"symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":8,"visibility":"visible"},"paint":{"text-color":"#990000"}},{"id":"road_label_cycling_shied","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Cycling","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["all",["in","network","icn"],["has","ref"]],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","icon-size":0.7,"symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":8,"visibility":"visible"},"paint":{"text-color":"#6dad3e"}},{"id":"place_label_other","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["in","type","neighbourhood","valley","glacier","locality","wetland","cliff","wineyard","ridge","wood","bev_ried","bev_gebiet"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,10],[15,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 33%, 1)","text-halo-color":"hsla(0, 0%, 100%, 0.6)","text-halo-width":1.1}},{"id":"poi_label_peaks","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Peaks","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["==","type","peak"],"layout":{"icon-image":"triangle-15","icon-size":0.4,"text-anchor":"top","text-field":"{name}\n{ele}","text-font":["Noto Sans Regular"],"text-offset":[0,0.5],"text-optional":false,"text-padding":{"base":1,"stops":[[9,35],[12,15]]},"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,15]]},"visibility":"visible"},"paint":{"text-color":"#333","text-opacity":0.9,"icon-opacity":0.8}},{"id":"road_label_pistes","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":13,"maxzoom":22,"filter":["==","type","piste"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":400,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.05,"text-max-angle":40,"text-padding":2,"text-pitch-alignment":"viewport","text-size":{"stops":[[8,8],[20,20]],"base":1.2},"visibility":"none"},"paint":{"text-color":"hsla(0, 0%, 0%, 0.8)","text-halo-color":"hsla(0, 0%, 100%, 1)","text-halo-width":{"stops":[[13,1],[20,2]]}}},{"id":"poi_label_mountain_pass","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Peaks"},"minzoom":11,"maxzoom":22,"filter":["==","type","moutain_pass"],"layout":{"icon-image":"triangle-stroked-15","icon-padding":20,"icon-size":1,"symbol-avoid-edges":false,"text-anchor":"top","text-field":"{name}\n{ele}","text-font":["Noto Sans Regular"],"text-offset":[0,0.5],"text-optional":false,"text-padding":20,"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,13]]},"visibility":"visible"},"paint":{"text-color":"#333"}},{"id":"poi_label_alpine_hut","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Peaks","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["==","subtype","alpine_hut"],"layout":{"icon-image":"alpine_hut","icon-padding":20,"icon-size":1,"symbol-avoid-edges":false,"text-anchor":"top","text-field":"{name}\n{ele}","text-font":["Noto Sans Regular"],"text-offset":[0,0.5],"text-optional":false,"text-padding":20,"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,13]]},"visibility":"visible"},"paint":{"text-color":"#333"}},{"id":"road_label_walking","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Labels","maptoolkit:type":"label"},"minzoom":13,"maxzoom":22,"filter":["in","network","iwn","nwn","rwn"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,8],[20,14]]},"visibility":"visible"},"paint":{"text-color":"#990000","text-halo-blur":0.5,"text-halo-color":"hsla(0, 0%, 100%, 0.8)","text-halo-width":0.9}},{"id":"road_label_cycling","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cycling"},"minzoom":11,"maxzoom":22,"filter":["in","network","icn","ncn","rcn"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,8],[20,14]]},"visibility":"visible"},"paint":{"text-color":"#6dad3e","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"road_label_2","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["all",["!in","type","ferry","motorway","primary","secondary","piste"],["!in","subtype","t-bar","drag_lift","magic_carpet","rope_tow","downhill","nordic"],["!has","network"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":200,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 0%, 0.6)","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"road_label_1","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Labels","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":8,"maxzoom":22,"filter":["all",["in","type","motorway","primary","secondary"],["!in","network","ncn","rcn","icn","lcn","iwn","nwn","rwn","lwn"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[13,10],[16,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 0%, 0.7)","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"road_label_highway_shield_primary","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Shields","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["==","type","primary"],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":10,"visibility":"visible"}},{"id":"road_label_highway_shield_motorway","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Shields","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["==","type","motorway"],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","icon-size":1,"symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":10,"visibility":"visible"}},{"id":"place_label_hamlet_suburb","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["in","type","hamlet","suburb"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,10],[15,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 33%, 1)","text-halo-color":"hsla(0, 0%, 100%, 0.6)","text-halo-width":1.1}},{"id":"poi_label_subway_label","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["all",["==","subtype","subway"],["==","agg_stop",1]],"layout":{"icon-image":"rail_metro","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":2,"text-pitch-alignment":"viewport","text-size":12,"visibility":"visible"},"paint":{"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"place_label_piste","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Villages","maptoolkit:type":"label"},"minzoom":8,"maxzoom":14,"filter":["==","type","piste"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":8,"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,12],[14,18]]},"visibility":"none"},"paint":{"text-color":"#0069a3","text-halo-color":"hsl(0, 0%, 100%)","text-halo-width":1,"text-opacity":0.6}},{"id":"place_label_village","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Villages","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":8,"maxzoom":22,"filter":["==","type","village"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":8,"text-padding":{"base":1,"stops":[[8,10],[11,2]]},"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,9.5],[16,18]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[8,"hsla(0, 0%, 33%, 1)"],[16,"hsla(0, 0%, 0%, 1)"]]},"text-halo-color":"hsla(0, 0%, 100%, 0.7)","text-halo-width":1}},{"id":"water_label_lakeline","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":3,"maxzoom":22,"filter":["all",["in","type","water","lake"],["==","$type","LineString"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.2,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[9,12],[20,18]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"water_label_point","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":3,"maxzoom":22,"filter":["all",["in","type","water","lake"],["==","$type","Point"],["<=","rank",15]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"point","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.2,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[9,10],[20,18]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"water_label_stream","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label"},"minzoom":12,"maxzoom":22,"filter":["all",["==","$type","LineString"],["in","type","stream"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":350,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[12,8],[18,14]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"water_label_river","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label"},"minzoom":12,"maxzoom":22,"filter":["all",["==","$type","LineString"],["in","type","river"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":350,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[3,10],[13,14],[18,20]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"hsla(0, 0%, 100%, 0.7)","text-halo-width":1.4}},{"id":"place_label_town_small","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Towns"},"minzoom":6,"maxzoom":22,"filter":["all",["==","type","town"],[">","rank",5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-padding":0,"icon-size":0.4,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[7,["Noto Sans Regular"]],[12,["Noto Sans Bold"]],[22,["Noto Sans Bold"]]]},"text-line-height":{"base":1,"stops":[[8,1],[20,1.3]]},"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-padding":{"base":1,"stops":[[6,10],[10,2]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[6,11],[12,15],[19,32],[20,48]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[7,"hsl(0, 0%, 0%)"],[18.9,"hsl(0, 0%, 0%)"],[19,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[18.9,"hsla(0, 0%, 100%, 0.6)"],[19,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[18.9,1],[19,0.5]]}}},{"id":"place_label_town_large","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Towns"},"minzoom":5,"maxzoom":22,"filter":["all",["==","type","town"],["<=","rank",5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-padding":0,"icon-size":0.4,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[6,["Noto Sans Regular"]],[11,["Noto Sans Bold"]],[22,["Noto Sans Bold"]]]},"text-line-height":{"base":1,"stops":[[8,1],[20,1.3]]},"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[6,12],[11,16],[18,32],[19,48],[19.9,48],[20,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[6,"hsl(0, 0%, 0%)"],[17.9,"hsl(0, 0%, 0%)"],[18,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[6,"hsla(0, 0%, 100%, 0.5)"],[17.9,"hsla(0, 0%, 100%, 0.5)"],[18,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[6,1],[17.9,1],[18,0.5]]}}},{"id":"place_label_island_small","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label"},"minzoom":8,"maxzoom":22,"filter":["all",["==","type","island"],[">","rank",3]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,12],[15,16]]},"visibility":"visible"},"paint":{"text-color":"#666","text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.2}},{"id":"place_label_island_large","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label"},"minzoom":7,"maxzoom":22,"filter":["all",["==","type","island"],["<","rank",4]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,12],[15,16]]},"visibility":"visible"},"paint":{"text-color":"#666","text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.2}},{"id":"place_label_city_small","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cities"},"minzoom":4,"maxzoom":15.9,"filter":["all",["==","type","city"],["!in","rank",0,1,2,3,4,5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-size":0.5,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[0,["Noto Sans Regular"]],[9,["Noto Sans Bold"]]]},"text-line-height":1.1,"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,12],[8,15],[9,18],[15,32],[16,48],[16.9,48],[17,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[0,"hsl(0, 0%, 0%)"],[13.9,"hsl(0, 0%, 0%)"],[14,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[5,"hsla(0, 0%, 100%, 0.5)"],[13,"hsla(0, 0%, 100%, 0.5)"],[14,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[5,1],[14,1],[15,0.5]]}}},{"id":"place_label_city_medium","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cities"},"minzoom":4,"maxzoom":15.9,"filter":["all",["==","type","city"],["in","rank",3,4,5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-size":0.5,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[0,["Noto Sans Regular"]],[9,["Noto Sans Bold"]]]},"text-line-height":1.1,"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,15],[7,18],[9,22],[14,48],[15,64],[15.9,64],[16,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[0,"hsl(0, 0%, 0%)"],[13.9,"hsl(0, 0%, 0%)"],[14,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[5,"hsla(0, 0%, 100%, 0.5)"],[13.9,"hsla(0, 0%, 100%, 0.5)"],[14,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[5,1],[13.9,1],[14,0.5]]}}},{"id":"natural_label_mountain_range","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Mountain Ranges","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":5,"maxzoom":12,"filter":["in","type","mountain_range"],"layout":{"symbol-placement":"line","text-field":"{name}","text-font":["Noto Sans Regular"],"text-letter-spacing":0.5,"text-max-width":100,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,8],[11,16]]},"visibility":"visible"},"paint":{"text-color":"hsl(0, 0%, 20%)","text-halo-color":"hsl(0, 0%, 100%)","text-halo-width":1.1}},{"id":"natural_label_protected_area","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Nature Reserve","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["all",["in","type","nature_reserve","protected_area"],["!=","subtype","protect_class_2"],["!=","seasonal","winter"]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.2,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":10,"stops":[[12,10],[15,13]]},"visibility":"visible"},"paint":{"text-color":"#425327","text-halo-color":"hsla(0, 0%, 100%, 0.6)","text-halo-width":1.1}},{"id":"natural_label_nationalpark","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Nature Reserve","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}},"nationalparks":{"layout":{"text-size":{"base":1.2,"stops":[[12,12],[15,16]]}}}}},"minzoom":5,"maxzoom":22,"filter":["in","type","national_park"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.2,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[10,12],[15,16]]},"visibility":"visible"},"paint":{"text-color":"#425327","text-halo-color":"hsla(0, 0%, 100%, 0.9)","text-halo-width":1.1}},{"id":"place_label_city_large","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cities"},"minzoom":4,"maxzoom":15.9,"filter":["all",["==","type","city"],["in","rank",0,1,2]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-size":0.6,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-padding":{"base":1,"stops":[[7,20],[8,5]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,15],[9,24],[13,48],[15,64],[17.9,64],[18,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[0,"hsl(0, 0%, 0%)"],[13.5,"hsl(0, 0%, 0%)"],[14,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[5,"hsla(0, 0%, 100%, 0.5)"],[13.5,"hsla(0, 0%, 100%, 0.5)"],[14,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[5,1],[13.5,1],[14,0.5]]}}},{"id":"water_label_ocean","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label"},"minzoom":1,"maxzoom":22,"filter":["in","type","sea","ocean"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"point","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.2,"text-line-height":1.6,"text-max-width":5,"text-offset":[0,1],"text-pitch-alignment":"viewport","text-size":{"stops":[[3,12],[4,16]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"place_label_country_4","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":3,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",4]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[4,11],[6,15]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}},{"id":"place_label_country_3","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":3,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",3]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[3,11],[7,17]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}},{"id":"place_label_country_2","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":2,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",2]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[2,11],[5,18]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}},{"id":"place_label_country_1","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":1,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",1]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[1,11],[4,17]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}}]} \ No newline at end of file diff --git a/Example/example/Toursprung.swift b/Example/example/Toursprung.swift new file mode 100644 index 000000000..54a2a9fad --- /dev/null +++ b/Example/example/Toursprung.swift @@ -0,0 +1,302 @@ +// +// Toursprung.swift +// Navigation +// +// Created by Patrick Kladek on 06.03.24. +// + +import Foundation +import MapboxCoreNavigation +import MapboxDirections +import MapboxNavigation +import MapLibre +import OSLog + +public typealias JSONDictionary = [String: Any] + +// MARK: - Toursprung + +public class Toursprung { + public enum ToursprungError: LocalizedError, Equatable { + case invalidUrl(message: String?) + case invalidResponse(message: String?) + case noRoute(message: String?) + case noSegment(message: String?) + case forbidden(message: String?) + case invalidInput(message: String?) + case profileNotFound(message: String?) + case notAuthorized(message: String?) + + public var errorDescription: String? { + switch self { + case let .invalidUrl(message): + errorDescription(message: message, defaultMessage: "Calculating route failed") + case let .invalidResponse(message): + errorDescription(message: message, defaultMessage: "Calculating route failed") + case let .noRoute(message): + errorDescription(message: message, defaultMessage: "No route found.") + case let .noSegment(message): + errorDescription(message: message, defaultMessage: "No segment found.") + case let .forbidden(message): + errorDescription(message: message, defaultMessage: "Forbidden access.") + case let .invalidInput(message): + errorDescription(message: message, defaultMessage: "Invalid input.") + case let .profileNotFound(message: message): + errorDescription(message: message, defaultMessage: "ProfileNotFound") + case let .notAuthorized(message: message): + errorDescription(message: message, defaultMessage: "NotAuthorized") + } + } + + public var failureReason: String? { + switch self { + case let .invalidUrl(message): + self.errorDescription(message: message, defaultMessage: "Calculating route failed because url can't be created") + case let .invalidResponse(message): + self.errorDescription(message: message, defaultMessage: "Hudhud responded with invalid route") + case let .noRoute(message): + self.errorDescription(message: message, defaultMessage: "No route found.") + case let .noSegment(message): + self.errorDescription(message: message, defaultMessage: "No segment found.") + case let .forbidden(message): + self.errorDescription(message: message, defaultMessage: "Forbidden access.") + case let .invalidInput(message): + self.errorDescription(message: message, defaultMessage: "Invalid input.") + case let .profileNotFound(message: message): + self.errorDescription(message: message, defaultMessage: "Profile Not Found") + case let .notAuthorized(message: message): + self.errorDescription(message: message, defaultMessage: "Not Authorized") + } + } + + public var recoverySuggestion: String? { + switch self { + case .invalidUrl: + "Retry with another destination" + case .invalidResponse: + "Update the app or retry with another destination" + case .noRoute: + "Retry with another destination" + case .noSegment: + "Retry with another destination" + case .forbidden: + "Forbidden access." + case .invalidInput: + "Invalid input." + case .profileNotFound: + "Profile Not Found" + case .notAuthorized: + "Not Authorized" + } + } + + public var helpAnchor: String? { + switch self { + case .invalidUrl: + "Search for another location and start navigation to there" + case .invalidResponse: + "Go to the AppStore and download the newest version of the App. Alternatively search for another location and start navigation to there." + case .noRoute: + "Search for another location and start navigation to there" + case .noSegment: + "Search for another location and start navigation to there" + case .forbidden: + "Forbidden access" + case .invalidInput: + "Invalid input" + case .profileNotFound: + "Profile Not Found" + case .notAuthorized: + "Not Authorized" + } + } + + // MARK: - Private + + private func errorDescription(message: String?, defaultMessage: String) -> String { + var description = defaultMessage + if let message { + description += " \(message)" + } + return description + } + } + + public typealias RouteCompletionHandler = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: Error?) -> Void + + public static let shared = Toursprung() + + // MARK: - Lifecycle + + public init() {} + + // MARK: - Public + + public struct RouteCalculationResult { + public let waypoints: [Waypoint] + public let routes: [Route] + } + + @discardableResult + public func calculate(_ options: RouteOptions) async throws -> RouteCalculationResult { + let url = try options.url + let answer: (data: Data, response: URLResponse) = try await URLSession.shared.data(from: url) + let json: JSONDictionary + + guard answer.response.mimeType == "application/json" else { + throw ToursprungError.invalidResponse(message: "MIME Type not matching application/json") + } + + do { + json = try JSONSerialization.jsonObject(with: answer.data, options: []) as? [String: Any] ?? [:] + } catch let error as ToursprungError { + throw ToursprungError.invalidResponse(message: "Route error occurred: \(error.localizedDescription)") + } + + let apiStatusCode = json["code"] as? String + let apiMessage = json["message"] as? String + guard (apiStatusCode == nil && apiMessage == nil) || apiStatusCode == "Ok" else { + switch apiStatusCode { + case "InvalidInput": + throw ToursprungError.invalidInput(message: apiMessage) + case "Not Authorized - No Token": + throw ToursprungError.notAuthorized(message: apiMessage) + case "Not Authorized - Invalid Token": + throw ToursprungError.notAuthorized(message: apiMessage) + case "Forbidden": + throw ToursprungError.forbidden(message: apiMessage) + case "ProfileNotFound": + throw ToursprungError.profileNotFound(message: apiMessage) + case "NoSegment": + throw ToursprungError.noSegment(message: apiMessage) + case "NoRoute": + throw ToursprungError.noRoute(message: apiMessage) + default: + throw ToursprungError.invalidResponse(message: nil) + } + } + + let response = try options.response(from: json) + for route in response.routes { + route.routeIdentifier = json["uuid"] as? String + } + guard let httpResponse = answer.response as? HTTPURLResponse else { + throw ToursprungError.invalidResponse(message: "Unexpected response type") + } + let httpStatusCode = httpResponse.statusCode + switch httpStatusCode { + case 500 ... 599: + throw ToursprungError.invalidResponse(message: "Server error HTTP status code: \(httpStatusCode)") + case 200 ... 299: + return RouteCalculationResult(waypoints: response.waypoint, routes: response.routes) + default: + throw ToursprungError.invalidResponse(message: "Server error occurred") + } + } +} + +// MARK: - Private + +private extension RouteOptions { + var url: URL { + get throws { + let stops = self.waypoints.map { "\($0.coordinate.longitude),\($0.coordinate.latitude)" }.joined(separator: ";") + + var components = URLComponents() + components.scheme = "https" + components.host = "gh.maptoolkit.net" + components.path = "/navigate/directions/v5/gh/car/\(stops)" + components.queryItems = [ + URLQueryItem(name: "access_token", value: ""), + URLQueryItem(name: "alternatives", value: "false"), + URLQueryItem(name: "geometries", value: "polyline6"), + URLQueryItem(name: "overview", value: "full"), + URLQueryItem(name: "steps", value: "true"), + URLQueryItem(name: "continue_straight", value: "true"), + URLQueryItem(name: "annotations", value: "congestion,distance"), + URLQueryItem(name: "language", value: Locale.preferredLanguages.first ?? "en-US"), + URLQueryItem(name: "roundabout_exits", value: "true"), + URLQueryItem(name: "voice_instructions", value: "true"), + URLQueryItem(name: "banner_instructions", value: "true"), + URLQueryItem(name: "voice_units", value: "metric") + ] + guard let url = components.url else { + throw Toursprung.ToursprungError.invalidUrl(message: "Couldn't create url from URLComponents") + } + + return url + } + } + + func response(from json: JSONDictionary) throws -> (waypoint: [Waypoint], routes: [Route]) { + var namedWaypoints: [Waypoint] = [] + if let jsonWaypoints = (json["waypoints"] as? [JSONDictionary]) { + namedWaypoints = try zip(jsonWaypoints, self.waypoints).compactMap { api, local -> Waypoint? in + guard let location = api["location"] as? [Double] else { + return nil + } + + let coordinate = try CLLocationCoordinate2D(geoJSON: location) + let possibleAPIName = api["name"] as? String + let apiName = possibleAPIName?.nonEmptyString + return Waypoint(coordinate: coordinate, name: local.name ?? apiName) + } + } + + let routes = (json["routes"] as? [JSONDictionary] ?? []).compactMap { + Route(json: $0, waypoints: waypoints, options: self) + } + return (namedWaypoints, routes) + } +} + +public extension CLLocationCoordinate2D { + enum GeoJSONError: LocalizedError { + case invalidCoordinates + case invalidType + + public var errorDescription: String? { + switch self { + case .invalidCoordinates: + "Can not read coordinates" + case .invalidType: + "Expecting different GeoJSON type" + } + } + + public var failureReason: String? { + switch self { + case .invalidCoordinates: + "data has more or less then 2 coordinates, expecting exactly 2" + case .invalidType: + "type should be either LineString or Point" + } + } + } + + init(geoJSON array: [Double]) throws { + guard array.count == 2 else { + throw GeoJSONError.invalidCoordinates + } + + self.init(latitude: array[1], longitude: array[0]) + } + + init(geoJSON point: JSONDictionary) throws { + guard point["type"] as? String == "Point" else { + throw GeoJSONError.invalidType + } + + try self.init(geoJSON: point["coordinates"] as? [Double] ?? []) + } + + static func coordinates(geoJSON lineString: JSONDictionary) throws -> [CLLocationCoordinate2D] { + let type = lineString["type"] as? String + guard type == "LineString" || type == "Point" else { + throw GeoJSONError.invalidType + } + + let coordinates = lineString["coordinates"] as? [[Double]] ?? [] + return try coordinates.map { try self.init(geoJSON: $0) } + } +} diff --git a/Example/example/ViewController.swift b/Example/example/ViewController.swift new file mode 100644 index 000000000..5ea4a8f36 --- /dev/null +++ b/Example/example/ViewController.swift @@ -0,0 +1,105 @@ +// +// ViewController.swift +// Example +// +// Created by Patrick Kladek on 16.05.24. +// + +import MapboxCoreNavigation +import MapboxDirections +import MapboxNavigation +import MapLibre + +class ViewController: UIViewController { + private let styleURL = Bundle.main.url(forResource: "Terrain", withExtension: "json")! // swiftlint:disable:this force_unwrapping + + var navigationView: NavigationMapView? + + // Keep `RouteController` in memory (class scope), + // otherwise location updates won't be triggered + public var mapboxRouteController: RouteController? + + override func viewDidLoad() { + super.viewDidLoad() + + let navigationView = NavigationMapView(frame: .zero, styleURL: self.styleURL, config: MNConfig()) + self.navigationView = navigationView + self.view.addSubview(navigationView) + + navigationView.translatesAutoresizingMaskIntoConstraints = false + navigationView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + let waypoints = [ + CLLocation(latitude: 52.032407, longitude: 5.580310), + CLLocation(latitude: 51.768686, longitude: 4.6827956) + ].map { Waypoint(location: $0) } + + Task { + do { + let routeOptions = NavigationRouteOptions(waypoints: waypoints) + let result = try await Toursprung.shared.calculate(routeOptions) + guard let route = result.routes.first else { return } + + await MainActor.run { + let simulatedLocationManager = SimulatedLocationManager(route: route) + simulatedLocationManager.speedMultiplier = 2 + + // let viewController = NavigationViewController(for: route, locationManager: simulatedLocationManager) + // viewController.mapView?.styleURL = self.styleURL + // self.present(viewController, animated: true) +// + let mapboxRouteController = RouteController(along: route, + directions: Directions.shared, + locationManager: simulatedLocationManager) + self.mapboxRouteController = mapboxRouteController + mapboxRouteController.delegate = self + mapboxRouteController.resume() + + NotificationCenter.default.addObserver(self, selector: #selector(self.didPassVisualInstructionPoint(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.didPassSpokenInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: nil) + + navigationView.showRoutes([route], legIndex: 0) + } + } catch { + print(error) + } + } + } +} + +// MARK: - RouteControllerDelegate + +extension ViewController: RouteControllerDelegate { + @objc + func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { + let camera = MLNMapCamera(lookingAtCenter: locations.first!.coordinate, acrossDistance: 500, pitch: 0, heading: 0) + + self.navigationView?.setCamera(camera, animated: true) + } + + @objc + func didPassVisualInstructionPoint(notification: NSNotification) { + guard let currentVisualInstruction = currentStepProgress(from: notification)?.currentVisualInstruction else { return } + + print(String( + format: "didPassVisualInstructionPoint primary text: %@ and secondary text: %@", + String(describing: currentVisualInstruction.primaryInstruction.text), + String(describing: currentVisualInstruction.secondaryInstruction?.text))) + } + + @objc + func didPassSpokenInstructionPoint(notification: NSNotification) { + guard let currentSpokenInstruction = currentStepProgress(from: notification)?.currentSpokenInstruction else { return } + + print("didPassSpokenInstructionPoint text: \(currentSpokenInstruction.text)") + } + + private + func currentStepProgress(from notification: NSNotification) -> RouteStepProgress? { + let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress + return routeProgress?.currentLegProgress.currentStepProgress + } +} From db580a07202a2d28368c7e25c4fd5891e5994ac1 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 16 May 2024 15:02:01 +0200 Subject: [PATCH 02/44] fix build --- Example/Example.xcodeproj/project.pbxproj | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 7e605e864..7069751e1 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ CD958B572BF6156200501F93 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CD958B562BF6156200501F93 /* Base */; }; CD958B622BF615D200501F93 /* Terrain.json in Resources */ = {isa = PBXBuildFile; fileRef = CD958B612BF615D200501F93 /* Terrain.json */; }; CD958B642BF6184900501F93 /* Toursprung.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B632BF6184900501F93 /* Toursprung.swift */; }; + CD958B682BF63B3500501F93 /* MapboxNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CD958B672BF63B3500501F93 /* MapboxNavigation */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -36,6 +37,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CD958B682BF63B3500501F93 /* MapboxNavigation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -48,6 +50,7 @@ CD958B652BF63A6700501F93 /* maplibre-navigation-ios */, CD958B492BF6156100501F93 /* example */, CD958B482BF6156100501F93 /* Products */, + CD958B662BF63B3500501F93 /* Frameworks */, ); sourceTree = ""; }; @@ -75,6 +78,13 @@ path = example; sourceTree = ""; }; + CD958B662BF63B3500501F93 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -92,6 +102,7 @@ ); name = Example; packageProductDependencies = ( + CD958B672BF63B3500501F93 /* MapboxNavigation */, ); productName = "navigation sample"; productReference = CD958B472BF6156100501F93 /* Example.app */; @@ -377,6 +388,13 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + CD958B672BF63B3500501F93 /* MapboxNavigation */ = { + isa = XCSwiftPackageProductDependency; + productName = MapboxNavigation; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = CD958B3F2BF6156100501F93 /* Project object */; } From ea7c5ed02e542e999208b7ccf107f2376542f08d Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 16 May 2024 16:41:46 +0200 Subject: [PATCH 03/44] add mapbox directions for calculating route --- Example/Example.xcodeproj/project.pbxproj | 4 ++ Example/example/Info.plist | 2 +- Example/example/Secrets.xcconfig | 11 ++++ Example/example/ViewController.swift | 61 ++++++++++++----------- 4 files changed, 48 insertions(+), 30 deletions(-) create mode 100644 Example/example/Secrets.xcconfig diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 7069751e1..80ee92cbb 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ CD958B612BF615D200501F93 /* Terrain.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Terrain.json; sourceTree = ""; }; CD958B632BF6184900501F93 /* Toursprung.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toursprung.swift; sourceTree = ""; }; CD958B652BF63A6700501F93 /* maplibre-navigation-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "maplibre-navigation-ios"; path = ..; sourceTree = ""; }; + CD958B692BF651F400501F93 /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -68,6 +69,7 @@ CD958B4A2BF6156100501F93 /* AppDelegate.swift */, CD958B4C2BF6156100501F93 /* SceneDelegate.swift */, CD958B4E2BF6156100501F93 /* ViewController.swift */, + CD958B692BF651F400501F93 /* Secrets.xcconfig */, CD958B632BF6184900501F93 /* Toursprung.swift */, CD958B612BF615D200501F93 /* Terrain.json */, CD958B502BF6156100501F93 /* Main.storyboard */, @@ -312,6 +314,7 @@ }; CD958B5C2BF6156200501F93 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = CD958B692BF651F400501F93 /* Secrets.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -340,6 +343,7 @@ }; CD958B5D2BF6156200501F93 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = CD958B692BF651F400501F93 /* Secrets.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; diff --git a/Example/example/Info.plist b/Example/example/Info.plist index 051525654..4d5288dc9 100644 --- a/Example/example/Info.plist +++ b/Example/example/Info.plist @@ -3,7 +3,7 @@ MGLMapboxAccessToken - empty + ${MAPBOX_TOKEN} UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Example/example/Secrets.xcconfig b/Example/example/Secrets.xcconfig new file mode 100644 index 000000000..f997102fe --- /dev/null +++ b/Example/example/Secrets.xcconfig @@ -0,0 +1,11 @@ +// +// Secrets.xcconfig +// Example +// +// Created by Patrick Kladek on 16.05.24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +MAPBOX_TOKEN= diff --git a/Example/example/ViewController.swift b/Example/example/ViewController.swift index 5ea4a8f36..a535e8ca4 100644 --- a/Example/example/ViewController.swift +++ b/Example/example/ViewController.swift @@ -37,35 +37,38 @@ class ViewController: UIViewController { CLLocation(latitude: 51.768686, longitude: 4.6827956) ].map { Waypoint(location: $0) } - Task { - do { - let routeOptions = NavigationRouteOptions(waypoints: waypoints) - let result = try await Toursprung.shared.calculate(routeOptions) - guard let route = result.routes.first else { return } - - await MainActor.run { - let simulatedLocationManager = SimulatedLocationManager(route: route) - simulatedLocationManager.speedMultiplier = 2 - - // let viewController = NavigationViewController(for: route, locationManager: simulatedLocationManager) - // viewController.mapView?.styleURL = self.styleURL - // self.present(viewController, animated: true) -// - let mapboxRouteController = RouteController(along: route, - directions: Directions.shared, - locationManager: simulatedLocationManager) - self.mapboxRouteController = mapboxRouteController - mapboxRouteController.delegate = self - mapboxRouteController.resume() - - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassVisualInstructionPoint(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassSpokenInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: nil) - - navigationView.showRoutes([route], legIndex: 0) - } - } catch { - print(error) - } + navigationView.zoomLevel = 12 + navigationView.centerCoordinate = waypoints[0].coordinate + + let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) + options.shapeFormat = .polyline6 + options.distanceMeasurementSystem = .metric + options.attributeOptions = [] + + print("[\(type(of: self))] Calculating routes with URL: \(Directions.shared.url(forCalculating: options))") + + Directions.shared.calculate(options) { _, routes, _ in + guard let route = routes?.first else { return } + + let simulatedLocationManager = SimulatedLocationManager(route: route) + simulatedLocationManager.speedMultiplier = 2 + + // let viewController = NavigationViewController(for: route, locationManager: simulatedLocationManager) + // viewController.mapView?.styleURL = self.styleURL + // self.present(viewController, animated: true) + + let mapboxRouteController = RouteController(along: route, + directions: Directions.shared, + locationManager: simulatedLocationManager) + self.mapboxRouteController = mapboxRouteController + mapboxRouteController.delegate = self + mapboxRouteController.resume() + + NotificationCenter.default.addObserver(self, selector: #selector(self.didPassVisualInstructionPoint(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.didPassSpokenInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: nil) + + navigationView.showRoutes([route], legIndex: 0) + navigationView.tracksUserCourse = true } } } From ee9b0a36c71ab4aea06de9c9986c0ff3776c6ded Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 17 May 2024 13:46:03 +0200 Subject: [PATCH 04/44] allow to start navigation after init --- Example/example/ViewController.swift | 24 +- .../NavigationViewController.swift | 329 ++++++++++-------- MapboxNavigation/RouteMapViewController.swift | 100 +++--- 3 files changed, 243 insertions(+), 210 deletions(-) diff --git a/Example/example/ViewController.swift b/Example/example/ViewController.swift index a535e8ca4..6127e02e8 100644 --- a/Example/example/ViewController.swift +++ b/Example/example/ViewController.swift @@ -47,28 +47,20 @@ class ViewController: UIViewController { print("[\(type(of: self))] Calculating routes with URL: \(Directions.shared.url(forCalculating: options))") + let viewController = NavigationViewController() + Directions.shared.calculate(options) { _, routes, _ in guard let route = routes?.first else { return } let simulatedLocationManager = SimulatedLocationManager(route: route) simulatedLocationManager.speedMultiplier = 2 - // let viewController = NavigationViewController(for: route, locationManager: simulatedLocationManager) - // viewController.mapView?.styleURL = self.styleURL - // self.present(viewController, animated: true) - - let mapboxRouteController = RouteController(along: route, - directions: Directions.shared, - locationManager: simulatedLocationManager) - self.mapboxRouteController = mapboxRouteController - mapboxRouteController.delegate = self - mapboxRouteController.resume() - - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassVisualInstructionPoint(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassSpokenInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: nil) - - navigationView.showRoutes([route], legIndex: 0) - navigationView.tracksUserCourse = true + viewController.mapView?.styleURL = self.styleURL + self.present(viewController, animated: true) { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { + viewController.begin(with: route, locationManager: simulatedLocationManager) + } + } } } } diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 7dc065358..02e064845 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -190,59 +190,79 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { - seealso: CarPlayNavigationViewController */ +@objcMembers @objc(MBNavigationViewController) open class NavigationViewController: UIViewController { + private var locationManager: NavigationLocationManager! + + var mapViewController: RouteMapViewController? + var styleManager: StyleManager! + + var currentStatusBarStyle: UIStatusBarStyle = .default { + didSet { + self.mapViewController?.instructionsBannerView.backgroundColor = InstructionsBannerView.appearance().backgroundColor + self.mapViewController?.instructionsBannerContentView.backgroundColor = InstructionsBannerContentView.appearance().backgroundColor + } + } + + // MARK: - Properties + /** A `Route` object constructed by [MapboxDirections](https://mapbox.github.io/mapbox-navigation-ios/directions/). In cases where you need to update the route after navigation has started you can set a new `route` here and `NavigationViewController` will update its UI accordingly. */ - @objc public var route: Route! { + public var route: Route? { didSet { - if self.routeController == nil { - self.routeController = RouteController(along: self.route, directions: self.directions, locationManager: self.locationManager) - self.routeController.delegate = self + if let route { + if self.routeController == nil { + let routeController = RouteController(along: route, directions: self.directions, locationManager: self.locationManager) + routeController.delegate = self + self.routeController = routeController + } else { + self.routeController?.routeProgress = RouteProgress(route: route) + } + NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile + self.mapViewController?.notifyDidReroute(route: route) } else { - self.routeController.routeProgress = RouteProgress(route: self.route) + self.routeController = nil } - NavigationSettings.shared.distanceUnit = self.route.routeOptions.locale.usesMetric ? .kilometer : .mile - self.mapViewController?.notifyDidReroute(route: self.route) } } /** An instance of `Directions` need for rerouting. See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. */ - @objc public var directions: Directions! + public var directions: Directions /** An optional `MLNMapCamera` you can use to improve the initial transition from a previous viewport and prevent a trigger from an excessive significant location update. */ - @objc public var pendingCamera: MLNMapCamera? + public var pendingCamera: MLNMapCamera? /** An instance of `MLNAnnotation` representing the origin of your route. */ - @objc public var origin: MLNAnnotation? + public var origin: MLNAnnotation? /** The receiver’s delegate. */ - @objc public weak var delegate: NavigationViewControllerDelegate? + public weak var delegate: NavigationViewControllerDelegate? /** Provides access to various speech synthesizer options. See `RouteVoiceController` for more information. */ - @objc public var voiceController: RouteVoiceController! + public var voiceController: RouteVoiceController? /** Provides all routing logic for the user. See `RouteController` for more information. */ - @objc public var routeController: RouteController! { + public var routeController: RouteController? { didSet { self.mapViewController?.routeController = self.routeController } @@ -253,7 +273,7 @@ open class NavigationViewController: UIViewController { - note: Do not change this map view’s delegate. */ - @objc public var mapView: NavigationMapView? { + public var mapView: NavigationMapView? { self.mapViewController?.mapView } @@ -262,17 +282,17 @@ open class NavigationViewController: UIViewController { By default, this property is set to `true`, causing the user location annotation to be snapped to the route. */ - @objc public var snapsUserLocationAnnotationToRoute = true + public var snapsUserLocationAnnotationToRoute = true /** Toggles sending of UILocalNotification upon upcoming steps when application is in the background. Defaults to `true`. */ - @objc public var sendsNotifications: Bool = true + public var sendsNotifications: Bool = true /** Shows a button that allows drivers to report feedback such as accidents, closed roads, poor instructions, etc. Defaults to `true`. */ - @objc public var showsReportFeedback: Bool = true { + public var showsReportFeedback: Bool = true { didSet { self.mapViewController?.reportButton.isHidden = !self.showsReportFeedback self.showsEndOfRouteFeedback = self.showsReportFeedback @@ -282,7 +302,7 @@ open class NavigationViewController: UIViewController { /** Shows End of route Feedback UI when the route controller arrives at the final destination. Defaults to `true.` */ - @objc public var showsEndOfRouteFeedback: Bool = true { + public var showsEndOfRouteFeedback: Bool = true { didSet { self.mapViewController?.showsEndOfRoute = self.showsEndOfRouteFeedback } @@ -291,7 +311,7 @@ open class NavigationViewController: UIViewController { /** If true, the map style and UI will automatically be updated given the time of day. */ - @objc public var automaticallyAdjustsStyleForTimeOfDay = true { + public var automaticallyAdjustsStyleForTimeOfDay = true { didSet { self.styleManager.automaticallyAdjustsStyleForTimeOfDay = self.automaticallyAdjustsStyleForTimeOfDay } @@ -300,48 +320,26 @@ open class NavigationViewController: UIViewController { /** If `true`, `UIApplication.isIdleTimerDisabled` is set to `true` in `viewWillAppear(_:)` and `false` in `viewWillDisappear(_:)`. If your application manages the idle timer itself, set this property to `false`. */ - @objc public var shouldManageApplicationIdleTimer = true + public var shouldManageApplicationIdleTimer = true /** Bool which should be set to true if a CarPlayNavigationView is also being used. */ - @objc public var isUsedInConjunctionWithCarPlayWindow = false { + public var isUsedInConjunctionWithCarPlayWindow = false { didSet { self.mapViewController?.isUsedInConjunctionWithCarPlayWindow = self.isUsedInConjunctionWithCarPlayWindow } } - var isConnectedToCarPlay: Bool { - if #available(iOS 12.0, *) { - CarPlayManager.shared.isConnectedToCarPlay - } else { - false - } - } - - var mapViewController: RouteMapViewController? - /** A Boolean value that determines whether the map annotates the locations at which instructions are spoken for debugging purposes. */ - @objc public var annotatesSpokenInstructions = false - - var styleManager: StyleManager! - - private var locationManager: NavigationLocationManager! - - var currentStatusBarStyle: UIStatusBarStyle = .default { - didSet { - self.mapViewController?.instructionsBannerView.backgroundColor = InstructionsBannerView.appearance().backgroundColor - self.mapViewController?.instructionsBannerContentView.backgroundColor = InstructionsBannerContentView.appearance().backgroundColor - } - } - - override open var preferredStatusBarStyle: UIStatusBarStyle { - self.currentStatusBarStyle - } + public var annotatesSpokenInstructions = false + // MARK: - Lifecycle + public required init?(coder aDecoder: NSCoder) { + self.directions = .shared super.init(coder: aDecoder) } @@ -351,31 +349,31 @@ open class NavigationViewController: UIViewController { See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. */ @objc(initWithRoute:directions:styles:routeController:locationManager:voiceController:) - public required init(for route: Route, + public required init(for route: Route? = nil, directions: Directions = Directions.shared, styles: [Style]? = [DayStyle(), NightStyle()], routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil, voiceController: RouteVoiceController? = nil) { - super.init(nibName: nil, bundle: nil) - + self.route = route + self.directions = directions self.locationManager = locationManager ?? NavigationLocationManager() - let routeController = routeController ?? RouteController(along: route, directions: directions, locationManager: self.locationManager) - self.routeController = routeController - self.routeController.usesDefaultUserInterface = true - self.routeController.delegate = self - self.routeController.tunnelIntersectionManager.delegate = self - + if let route { + self.routeController = routeController ?? RouteController(along: route, directions: directions, locationManager: self.locationManager) + NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile + } self.voiceController = voiceController ?? RouteVoiceController() - - self.directions = directions - self.route = route - NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile - routeController.resume() + + super.init(nibName: nil, bundle: nil) + + self.routeController?.usesDefaultUserInterface = true + self.routeController?.delegate = self + self.routeController?.tunnelIntersectionManager.delegate = self + self.routeController?.resume() let mapViewController = RouteMapViewController(routeController: self.routeController, delegate: self) self.mapViewController = mapViewController - mapViewController.destination = route.legs.last?.destination + mapViewController.destination = route?.legs.last?.destination mapViewController.willMove(toParent: self) addChild(mapViewController) mapViewController.didMove(toParent: self) @@ -389,22 +387,28 @@ open class NavigationViewController: UIViewController { self.styleManager = StyleManager(self) self.styleManager.styles = styles ?? [DayStyle(), NightStyle()] - if !(route.routeOptions is NavigationRouteOptions) { + if !(route?.routeOptions is NavigationRouteOptions) { print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") } } deinit { - suspendNotifications() + self.suspendNotifications() } + // MARK: - UIViewController + + override open var preferredStatusBarStyle: UIStatusBarStyle { + self.currentStatusBarStyle + } + override open func viewDidLoad() { super.viewDidLoad() // Initialize voice controller if it hasn't been overridden. // This is optional and lazy so it can be mutated by the developer after init. _ = self.voiceController self.resumeNotifications() - view.clipsToBounds = true + self.view.clipsToBounds = true } override open func viewWillAppear(_ animated: Bool) { @@ -414,7 +418,7 @@ open class NavigationViewController: UIViewController { UIApplication.shared.isIdleTimerDisabled = true } - if self.routeController.locationManager is SimulatedLocationManager { + if self.routeController?.locationManager is SimulatedLocationManager { let localized = String.Localized.simulationStatus(speed: 1) self.mapViewController?.statusView.show(localized, showSpinner: false, interactive: true) } @@ -427,76 +431,17 @@ open class NavigationViewController: UIViewController { UIApplication.shared.isIdleTimerDisabled = false } - self.routeController.suspendLocationUpdates() - } - - // MARK: Route controller notifications - - func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(self.progressDidChange(notification:)), name: .routeControllerProgressDidChange, object: self.routeController) - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) - } - - func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: .routeControllerProgressDidChange, object: self.routeController) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) - } - - @objc func progressDidChange(notification: NSNotification) { - let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress - let location = notification.userInfo![RouteControllerNotificationUserInfoKey.locationKey] as! CLLocation - let secondsRemaining = routeProgress.currentLegProgress.currentStepProgress.durationRemaining - - self.mapViewController?.notifyDidChange(routeProgress: routeProgress, location: location, secondsRemaining: secondsRemaining) - - // If the user has arrived, don't snap the user puck. - // In the case the user drives beyond the waypoint, - // we should accurately depict this. - let shouldPreventReroutesWhenArrivingAtWaypoint = self.routeController.delegate?.routeController?(self.routeController, shouldPreventReroutesWhenArrivingAt: self.routeController.routeProgress.currentLeg.destination) ?? true - let userHasArrivedAndShouldPreventRerouting = shouldPreventReroutesWhenArrivingAtWaypoint && !self.routeController.routeProgress.currentLegProgress.userHasArrivedAtWaypoint - - if self.snapsUserLocationAnnotationToRoute, - userHasArrivedAndShouldPreventRerouting { - self.mapViewController?.mapView.updateCourseTracking(location: location, animated: true) - } - } - - @objc func didPassInstructionPoint(notification: NSNotification) { - let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress - - self.mapViewController?.updateCameraAltitude(for: routeProgress) - - self.clearStaleNotifications() - - if routeProgress.currentLegProgress.currentStepProgress.durationRemaining <= RouteControllerHighAlertInterval { - self.scheduleLocalNotification(about: routeProgress.currentLegProgress.currentStep, legIndex: routeProgress.legIndex, numberOfLegs: routeProgress.route.legs.count) - } - } - - func scheduleLocalNotification(about step: RouteStep, legIndex: Int?, numberOfLegs: Int?) { - guard self.sendsNotifications else { return } - guard UIApplication.shared.applicationState == .background else { return } - guard let text = step.instructionsSpokenAlongStep?.last?.text else { return } - - let notification = UILocalNotification() - notification.alertBody = text - notification.fireDate = Date() - - self.clearStaleNotifications() - - UIApplication.shared.cancelAllLocalNotifications() - UIApplication.shared.scheduleLocalNotification(notification) + self.routeController?.suspendLocationUpdates() } - func clearStaleNotifications() { - guard self.sendsNotifications else { return } - // Remove all outstanding notifications from notification center. - // This will only work if it's set to 1 and then back to 0. - // This way, there is always just one notification. - UIApplication.shared.applicationIconBadgeNumber = 1 - UIApplication.shared.applicationIconBadgeNumber = 0 + // MARK: - NavigationViewController + + public func begin(with route: Route, locationManager: NavigationLocationManager? = nil) { + self.locationManager = locationManager + self.route = route + self.routeController?.resume() } - + #if canImport(CarPlay) /** Presents a `NavigationViewController` on the top most view controller in the window and opens up the `StepsViewController`. @@ -551,11 +496,11 @@ extension NavigationViewController: RouteMapViewControllerDelegate { self.delegate?.navigationViewController?(self, didSelect: route) } - @objc public func navigationMapView(_ mapView: NavigationMapView, shapeFor routes: [Route]) -> MLNShape? { + public func navigationMapView(_ mapView: NavigationMapView, shapeFor routes: [Route]) -> MLNShape? { self.delegate?.navigationViewController?(self, shapeFor: routes) } - @objc public func navigationMapView(_ mapView: NavigationMapView, simplifiedShapeFor route: Route) -> MLNShape? { + public func navigationMapView(_ mapView: NavigationMapView, simplifiedShapeFor route: Route) -> MLNShape? { self.delegate?.navigationViewController?(self, simplifiedShapeFor: route) } @@ -567,15 +512,15 @@ extension NavigationViewController: RouteMapViewControllerDelegate { self.delegate?.navigationViewController?(self, waypointSymbolStyleLayerWithIdentifier: identifier, source: source) } - @objc public func navigationMapView(_ mapView: NavigationMapView, shapeFor waypoints: [Waypoint], legIndex: Int) -> MLNShape? { + public func navigationMapView(_ mapView: NavigationMapView, shapeFor waypoints: [Waypoint], legIndex: Int) -> MLNShape? { self.delegate?.navigationViewController?(self, shapeFor: waypoints, legIndex: legIndex) } - @objc public func navigationMapView(_ mapView: MLNMapView, imageFor annotation: MLNAnnotation) -> MLNAnnotationImage? { + public func navigationMapView(_ mapView: MLNMapView, imageFor annotation: MLNAnnotation) -> MLNAnnotationImage? { self.delegate?.navigationViewController?(self, imageFor: annotation) } - @objc public func navigationMapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? { + public func navigationMapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? { self.delegate?.navigationViewController?(self, viewFor: annotation) } @@ -595,14 +540,14 @@ extension NavigationViewController: RouteMapViewControllerDelegate { self.annotatesSpokenInstructions } - @objc func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? { + func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? { guard let roadName = delegate?.navigationViewController?(self, roadNameAt: location) else { return nil } return roadName } - @objc public func label(_ label: InstructionLabel, willPresent instruction: VisualInstruction, as presented: NSAttributedString) -> NSAttributedString? { + public func label(_ label: InstructionLabel, willPresent instruction: VisualInstruction, as presented: NSAttributedString) -> NSAttributedString? { self.delegate?.label?(label, willPresent: instruction, as: presented) } } @@ -610,28 +555,28 @@ extension NavigationViewController: RouteMapViewControllerDelegate { // MARK: - RouteControllerDelegate extension NavigationViewController: RouteControllerDelegate { - @objc public func routeController(_ routeController: RouteController, shouldRerouteFrom location: CLLocation) -> Bool { + public func routeController(_ routeController: RouteController, shouldRerouteFrom location: CLLocation) -> Bool { self.delegate?.navigationViewController?(self, shouldRerouteFrom: location) ?? true } - @objc public func routeController(_ routeController: RouteController, willRerouteFrom location: CLLocation) { + public func routeController(_ routeController: RouteController, willRerouteFrom location: CLLocation) { self.delegate?.navigationViewController?(self, willRerouteFrom: location) } - @objc public func routeController(_ routeController: RouteController, didRerouteAlong route: Route) { + public func routeController(_ routeController: RouteController, didRerouteAlong route: Route) { self.mapViewController?.notifyDidReroute(route: route) self.delegate?.navigationViewController?(self, didRerouteAlong: route) } - @objc public func routeController(_ routeController: RouteController, didFailToRerouteWith error: Error) { + public func routeController(_ routeController: RouteController, didFailToRerouteWith error: Error) { self.delegate?.navigationViewController?(self, didFailToRerouteWith: error) } - @objc public func routeController(_ routeController: RouteController, shouldDiscard location: CLLocation) -> Bool { + public func routeController(_ routeController: RouteController, shouldDiscard location: CLLocation) -> Bool { self.delegate?.navigationViewController?(self, shouldDiscard: location) ?? true } - @objc public func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { + public func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { // If the user has arrived, don't snap the user puck. // In the case the user drives beyond the waypoint, // we should accurately depict this. @@ -648,7 +593,7 @@ extension NavigationViewController: RouteControllerDelegate { } } - @objc public func routeController(_ routeController: RouteController, didArriveAt waypoint: Waypoint) -> Bool { + public func routeController(_ routeController: RouteController, didArriveAt waypoint: Waypoint) -> Bool { let advancesToNextLeg = self.delegate?.navigationViewController?(self, didArriveAt: waypoint) ?? true if !self.isConnectedToCarPlay, // CarPlayManager shows rating on CarPlay if it's connected @@ -659,23 +604,27 @@ extension NavigationViewController: RouteControllerDelegate { } } +// MARK: - TunnelIntersectionManagerDelegate + extension NavigationViewController: TunnelIntersectionManagerDelegate { public func tunnelIntersectionManager(_ manager: TunnelIntersectionManager, willEnableAnimationAt location: CLLocation) { - self.routeController.tunnelIntersectionManager(manager, willEnableAnimationAt: location) + self.routeController?.tunnelIntersectionManager(manager, willEnableAnimationAt: location) self.styleManager.applyStyle(type: .night) } public func tunnelIntersectionManager(_ manager: TunnelIntersectionManager, willDisableAnimationAt location: CLLocation) { - self.routeController.tunnelIntersectionManager(manager, willDisableAnimationAt: location) + self.routeController?.tunnelIntersectionManager(manager, willDisableAnimationAt: location) self.styleManager.timeOfDayChanged() } } +// MARK: - StyleManagerDelegate + extension NavigationViewController: StyleManagerDelegate { public func locationFor(styleManager: StyleManager) -> CLLocation? { - if let location = routeController.location { + if let location = self.routeController?.location { location - } else if let firstCoord = routeController.routeProgress.route.coordinates?.first { + } else if let firstCoord = self.routeController?.routeProgress.route.coordinates?.first { CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) } else { nil @@ -696,3 +645,81 @@ extension NavigationViewController: StyleManagerDelegate { self.mapView?.reloadStyle(self) } } + +// MARK: - Private + +private extension NavigationViewController { + var isConnectedToCarPlay: Bool { + if #available(iOS 12.0, *) { + CarPlayManager.shared.isConnectedToCarPlay + } else { + false + } + } + + func resumeNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(self.progressDidChange(notification:)), name: .routeControllerProgressDidChange, object: self.routeController) + NotificationCenter.default.addObserver(self, selector: #selector(self.didPassInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) + } + + func suspendNotifications() { + NotificationCenter.default.removeObserver(self, name: .routeControllerProgressDidChange, object: self.routeController) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassSpokenInstructionPoint, object: self.routeController) + } + + @objc func progressDidChange(notification: NSNotification) { + let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress + let location = notification.userInfo![RouteControllerNotificationUserInfoKey.locationKey] as! CLLocation + let secondsRemaining = routeProgress.currentLegProgress.currentStepProgress.durationRemaining + + self.mapViewController?.notifyDidChange(routeProgress: routeProgress, location: location, secondsRemaining: secondsRemaining) + guard let routeController else { return } + + // If the user has arrived, don't snap the user puck. + // In the case the user drives beyond the waypoint, + // we should accurately depict this. + let shouldPreventReroutesWhenArrivingAtWaypoint = routeController.delegate?.routeController?(routeController, shouldPreventReroutesWhenArrivingAt: routeController.routeProgress.currentLeg.destination) ?? true + let userHasArrivedAndShouldPreventRerouting = shouldPreventReroutesWhenArrivingAtWaypoint && !routeController.routeProgress.currentLegProgress.userHasArrivedAtWaypoint + + if self.snapsUserLocationAnnotationToRoute, + userHasArrivedAndShouldPreventRerouting { + self.mapViewController?.mapView.updateCourseTracking(location: location, animated: true) + } + } + + @objc func didPassInstructionPoint(notification: NSNotification) { + let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress + + self.mapViewController?.updateCameraAltitude(for: routeProgress) + + self.clearStaleNotifications() + + if routeProgress.currentLegProgress.currentStepProgress.durationRemaining <= RouteControllerHighAlertInterval { + self.scheduleLocalNotification(about: routeProgress.currentLegProgress.currentStep, legIndex: routeProgress.legIndex, numberOfLegs: routeProgress.route.legs.count) + } + } + + func scheduleLocalNotification(about step: RouteStep, legIndex: Int?, numberOfLegs: Int?) { + guard self.sendsNotifications else { return } + guard UIApplication.shared.applicationState == .background else { return } + guard let text = step.instructionsSpokenAlongStep?.last?.text else { return } + + let notification = UILocalNotification() + notification.alertBody = text + notification.fireDate = Date() + + self.clearStaleNotifications() + + UIApplication.shared.cancelAllLocalNotifications() + UIApplication.shared.scheduleLocalNotification(notification) + } + + func clearStaleNotifications() { + guard self.sendsNotifications else { return } + // Remove all outstanding notifications from notification center. + // This will only work if it's set to 1 and then back to 0. + // This way, there is always just one notification. + UIApplication.shared.applicationIconBadgeNumber = 1 + UIApplication.shared.applicationIconBadgeNumber = 0 + } +} diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 57ae91cf7..2e0e488d0 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -30,7 +30,7 @@ class RouteMapViewController: UIViewController { static let recenter: Selector = #selector(RouteMapViewController.recenter(_:)) } - var route: Route { self.routeController.routeProgress.route } + var route: Route? { self.routeController?.routeProgress.route } var updateETATimer: Timer? var previewInstructionsView: StepInstructionsView? var lastTimeUserRerouted: Date? @@ -64,10 +64,11 @@ class RouteMapViewController: UIViewController { } weak var delegate: RouteMapViewControllerDelegate? - var routeController: Router! { + var routeController: Router? { didSet { - self.navigationView.statusView.canChangeValue = self.routeController.locationManager is SimulatedLocationManager - guard let destination = route.legs.last?.destination else { return } + self.navigationView.statusView.canChangeValue = self.routeController?.locationManager is SimulatedLocationManager + guard let destination = self.route?.legs.last?.destination else { return } + self.populateName(for: destination, populated: { self.destination = $0 }) } } @@ -105,11 +106,11 @@ class RouteMapViewController: UIViewController { var labelRoadNameCompletionHandler: LabelRoadNameCompletionHandler? - convenience init(routeController: RouteController, delegate: RouteMapViewControllerDelegate? = nil) { + convenience init(routeController: RouteController?, delegate: RouteMapViewControllerDelegate? = nil) { self.init() self.routeController = routeController self.delegate = delegate - automaticallyAdjustsScrollViewInsets = false + self.automaticallyAdjustsScrollViewInsets = false } override func loadView() { @@ -151,12 +152,12 @@ class RouteMapViewController: UIViewController { if let camera = pendingCamera { self.mapView.camera = camera - } else if let location = routeController.location, location.course > 0 { + } else if let location = self.routeController?.location, location.course > 0 { self.mapView.updateCourseTracking(location: location, animated: false) - } else if let coordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { + } else if let coordinates = self.routeController?.routeProgress.currentLegProgress.currentStep.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { let secondCoordinate = coordinates[1] let course = firstCoordinate.direction(to: secondCoordinate) - let newLocation = CLLocation(coordinate: routeController.location?.coordinate ?? firstCoordinate, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: course, speed: 0, timestamp: Date()) + let newLocation = CLLocation(coordinate: self.routeController?.location?.coordinate ?? firstCoordinate, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: course, speed: 0, timestamp: Date()) self.mapView.updateCourseTracking(location: newLocation, animated: false) } else { self.mapView.setCamera(self.tiltedCamera, animated: false) @@ -167,8 +168,8 @@ class RouteMapViewController: UIViewController { super.viewDidAppear(animated) self.annotatesSpokenInstructions = self.delegate?.mapViewControllerShouldAnnotateSpokenInstructions(self) ?? false showRouteIfNeeded() - self.currentLegIndexMapped = self.routeController.routeProgress.legIndex - self.currentStepIndexMapped = self.routeController.routeProgress.currentLegProgress.stepIndex + self.currentLegIndexMapped = self.routeController?.routeProgress.legIndex ?? 0 + self.currentStepIndexMapped = self.routeController?.routeProgress.currentLegProgress.stepIndex ?? 0 } override func viewWillDisappear(_ animated: Bool) { @@ -200,11 +201,13 @@ class RouteMapViewController: UIViewController { self.mapView.tracksUserCourse = true self.mapView.enableFrameByFrameCourseViewTracking(for: 3) self.isInOverviewMode = false - self.updateCameraAltitude(for: self.routeController.routeProgress) + guard let routeController else { return } + + self.updateCameraAltitude(for: routeController.routeProgress) - self.mapView.addArrow(route: self.routeController.routeProgress.route, - legIndex: self.routeController.routeProgress.legIndex, - stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) + self.mapView.addArrow(route: routeController.routeProgress.route, + legIndex: routeController.routeProgress.legIndex, + stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) self.removePreviewInstructions() } @@ -225,7 +228,8 @@ class RouteMapViewController: UIViewController { @objc func toggleOverview(_ sender: Any) { self.mapView.enableFrameByFrameCourseViewTracking(for: 3) - if let coordinates = routeController.routeProgress.route.coordinates, let userLocation = routeController.locationManager.location?.coordinate { + if let coordinates = self.routeController?.routeProgress.route.coordinates, + let userLocation = self.routeController?.locationManager.location?.coordinate { self.mapView.setOverheadCameraView(from: userLocation, along: coordinates, for: self.overheadInsets) } self.isInOverviewMode = true @@ -250,17 +254,18 @@ class RouteMapViewController: UIViewController { } func notifyDidReroute(route: Route) { - updateETA() + self.updateETA() self.currentStepIndexMapped = 0 + guard let routeController else { return } + + self.instructionsBannerView.updateDistance(for: routeController.routeProgress.currentLegProgress.currentStepProgress) - self.instructionsBannerView.updateDistance(for: self.routeController.routeProgress.currentLegProgress.currentStepProgress) - - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex, stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) - self.mapView.showRoutes([self.routeController.routeProgress.route], legIndex: self.routeController.routeProgress.legIndex) - self.mapView.showWaypoints(self.routeController.routeProgress.route) + self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) + self.mapView.showRoutes([routeController.routeProgress.route], legIndex: routeController.routeProgress.legIndex) + self.mapView.showWaypoints(routeController.routeProgress.route) if self.annotatesSpokenInstructions { - self.mapView.showVoiceInstructionsOnMap(route: self.routeController.routeProgress.route) + self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) } if self.isInOverviewMode { @@ -280,7 +285,7 @@ class RouteMapViewController: UIViewController { } @objc func applicationWillEnterForeground(notification: NSNotification) { - self.mapView.updateCourseTracking(location: self.routeController.location, animated: false) + self.mapView.updateCourseTracking(location: self.routeController?.location, animated: false) resetETATimer() } @@ -295,7 +300,7 @@ class RouteMapViewController: UIViewController { } func notifyUserAboutLowVolume() { - guard !(self.routeController.locationManager is SimulatedLocationManager) else { return } + guard !(self.routeController?.locationManager is SimulatedLocationManager) else { return } guard !NavigationSettings.shared.voiceMuted else { return } guard AVAudioSession.sharedInstance().outputVolume <= NavigationViewMinimumVolumeForWarning else { return } @@ -307,7 +312,7 @@ class RouteMapViewController: UIViewController { @objc func didReroute(notification: NSNotification) { guard isViewLoaded else { return } - if let locationManager = routeController.locationManager as? SimulatedLocationManager { + if let locationManager = self.routeController?.locationManager as? SimulatedLocationManager { let localized = String.Localized.simulationStatus(speed: Int(locationManager.speedMultiplier)) self.showStatus(title: localized, for: .infinity, interactive: true) } else { @@ -328,8 +333,12 @@ class RouteMapViewController: UIViewController { } func updateMapOverlays(for routeProgress: RouteProgress) { + guard let routeController else { return } + if routeProgress.currentLegProgress.followOnStep != nil { - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex, stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) + self.mapView.addArrow(route: routeController.routeProgress.route, + legIndex: routeController.routeProgress.legIndex, + stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) } else { self.mapView.removeArrow() } @@ -395,8 +404,8 @@ class RouteMapViewController: UIViewController { self.currentStepIndexMapped = routeProgress.currentLegProgress.stepIndex } - if self.annotatesSpokenInstructions { - self.mapView.showVoiceInstructionsOnMap(route: self.routeController.routeProgress.route) + if self.annotatesSpokenInstructions, let routeController { + self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) } } @@ -416,7 +425,7 @@ class RouteMapViewController: UIViewController { endOfRoute.didMove(toParent: self) endOfRoute.dismissHandler = { [weak self] _, _ in - self?.routeController.endNavigation() + self?.routeController?.endNavigation() self?.delegate?.mapViewControllerDidDismiss(self!, byCanceling: false) } } @@ -456,7 +465,7 @@ class RouteMapViewController: UIViewController { guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } let insets = UIEdgeInsets(top: navigationView.instructionsBannerView.bounds.height, left: 20, bottom: height + 20, right: 20) - if let coordinates = routeController.routeProgress.route.coordinates, let userLocation = routeController?.locationManager.location?.coordinate { + if let coordinates = self.routeController?.routeProgress.route.coordinates, let userLocation = routeController?.locationManager.location?.coordinate { let slicedLine = Polyline(coordinates).sliced(from: userLocation).coordinates let line = MLNPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) @@ -582,7 +591,7 @@ extension RouteMapViewController: NavigationViewDelegate { if let controller = stepsViewController { self.stepsViewController = nil controller.dismiss() - } else { + } else if let routeController { let controller = StepsViewController(routeProgress: routeController.routeProgress) controller.delegate = self addChild(controller) @@ -682,7 +691,7 @@ extension RouteMapViewController: NavigationViewDelegate { } func labelCurrentRoadFeature(at location: CLLocation) { - guard let style = mapView.style, let stepCoordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates else { + guard let style = self.mapView.style, let stepCoordinates = self.routeController?.routeProgress.currentLegProgress.currentStep.coordinates else { return } @@ -820,8 +829,9 @@ extension RouteMapViewController: NavigationViewDelegate { } @objc func updateETA() { - guard isViewLoaded, self.routeController != nil else { return } - self.navigationView.bottomBannerView.updateETA(routeProgress: self.routeController.routeProgress) + guard isViewLoaded, let routeController else { return } + + self.navigationView.bottomBannerView.updateETA(routeProgress: routeController.routeProgress) } func resetETATimer() { @@ -830,17 +840,19 @@ extension RouteMapViewController: NavigationViewDelegate { } func showRouteIfNeeded() { - guard isViewLoaded, view.window != nil else { return } + guard self.isViewLoaded, self.view.window != nil else { return } guard !self.mapView.showsRoute else { return } - self.mapView.showRoutes([self.routeController.routeProgress.route], legIndex: self.routeController.routeProgress.legIndex) - self.mapView.showWaypoints(self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex) + guard let routeController else { return } + + self.mapView.showRoutes([routeController.routeProgress.route], legIndex: routeController.routeProgress.legIndex) + self.mapView.showWaypoints(routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex) - if self.routeController.routeProgress.currentLegProgress.stepIndex + 1 <= self.routeController.routeProgress.currentLegProgress.leg.steps.count { - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: self.routeController.routeProgress.legIndex, stepIndex: self.routeController.routeProgress.currentLegProgress.stepIndex + 1) + if routeController.routeProgress.currentLegProgress.stepIndex + 1 <= routeController.routeProgress.currentLegProgress.leg.steps.count { + self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) } if self.annotatesSpokenInstructions { - self.mapView.showVoiceInstructionsOnMap(route: self.routeController.routeProgress.route) + self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) } } } @@ -849,6 +861,8 @@ extension RouteMapViewController: NavigationViewDelegate { extension RouteMapViewController: StepsViewControllerDelegate { func stepsViewController(_ viewController: StepsViewController, didSelect legIndex: Int, stepIndex: Int, cell: StepTableViewCell) { + guard let routeController else { return } + let legProgress = RouteLegProgress(leg: routeController.routeProgress.route.legs[legIndex], stepIndex: stepIndex) let step = legProgress.currentStep guard let upcomingStep = legProgress.upComingStep else { return } @@ -863,7 +877,7 @@ extension RouteMapViewController: StepsViewControllerDelegate { self.mapView.setCenter(upcomingStep.maneuverLocation, zoomLevel: self.mapView.zoomLevel, direction: upcomingStep.initialHeading!, animated: true, completionHandler: nil) guard isViewLoaded, view.window != nil else { return } - self.mapView.addArrow(route: self.routeController.routeProgress.route, legIndex: legIndex, stepIndex: stepIndex + 1) + self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: legIndex, stepIndex: stepIndex + 1) } func addPreviewInstructions(step: RouteStep, maneuverStep: RouteStep, distance: CLLocationDistance?) { @@ -895,7 +909,7 @@ extension RouteMapViewController: StepsViewControllerDelegate { let title = String.Localized.simulationStatus(speed: displayValue) self.showStatus(title: title, for: .infinity, interactive: true) - if let locationManager = routeController.locationManager as? SimulatedLocationManager { + if let locationManager = self.routeController?.locationManager as? SimulatedLocationManager { locationManager.speedMultiplier = Double(displayValue) } } From 1ff2c04df0e8ecd81a54fab5d3936e39777ced05 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 17 May 2024 16:33:24 +0200 Subject: [PATCH 05/44] remove storyboard from example app --- Example/Example.xcodeproj/project.pbxproj | 15 +- .../xcshareddata/xcschemes/Example.xcscheme | 78 +++++++++ Example/example/Base.lproj/Main.storyboard | 24 --- Example/example/Info.plist | 2 - Example/example/SceneDelegate.swift | 43 ++++- Example/example/ViewController.swift | 124 ++++++-------- MapboxNavigation/NavigationView.swift | 2 +- .../NavigationViewController.swift | 6 +- MapboxNavigation/NavigationViewLayout.swift | 73 ++++---- MapboxNavigation/RouteMapViewController.swift | 156 +++++++++--------- 10 files changed, 292 insertions(+), 231 deletions(-) create mode 100644 Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme delete mode 100644 Example/example/Base.lproj/Main.storyboard diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 80ee92cbb..6f505f9c2 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ CD958B4B2BF6156100501F93 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4A2BF6156100501F93 /* AppDelegate.swift */; }; CD958B4D2BF6156100501F93 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4C2BF6156100501F93 /* SceneDelegate.swift */; }; CD958B4F2BF6156100501F93 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4E2BF6156100501F93 /* ViewController.swift */; }; - CD958B522BF6156100501F93 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CD958B512BF6156100501F93 /* Base */; }; CD958B542BF6156200501F93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD958B532BF6156200501F93 /* Assets.xcassets */; }; CD958B572BF6156200501F93 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CD958B562BF6156200501F93 /* Base */; }; CD958B622BF615D200501F93 /* Terrain.json in Resources */ = {isa = PBXBuildFile; fileRef = CD958B612BF615D200501F93 /* Terrain.json */; }; @@ -23,7 +22,6 @@ CD958B4A2BF6156100501F93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CD958B4C2BF6156100501F93 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; CD958B4E2BF6156100501F93 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - CD958B512BF6156100501F93 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; CD958B532BF6156200501F93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CD958B562BF6156200501F93 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; CD958B582BF6156200501F93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -54,6 +52,7 @@ CD958B662BF63B3500501F93 /* Frameworks */, ); sourceTree = ""; + usesTabs = 0; }; CD958B482BF6156100501F93 /* Products */ = { isa = PBXGroup; @@ -72,7 +71,6 @@ CD958B692BF651F400501F93 /* Secrets.xcconfig */, CD958B632BF6184900501F93 /* Toursprung.swift */, CD958B612BF615D200501F93 /* Terrain.json */, - CD958B502BF6156100501F93 /* Main.storyboard */, CD958B532BF6156200501F93 /* Assets.xcassets */, CD958B552BF6156200501F93 /* LaunchScreen.storyboard */, CD958B582BF6156200501F93 /* Info.plist */, @@ -153,7 +151,6 @@ CD958B542BF6156200501F93 /* Assets.xcassets in Resources */, CD958B622BF615D200501F93 /* Terrain.json in Resources */, CD958B572BF6156200501F93 /* Base in Resources */, - CD958B522BF6156100501F93 /* Base in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -174,14 +171,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - CD958B502BF6156100501F93 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - CD958B512BF6156100501F93 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; CD958B552BF6156200501F93 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -325,7 +314,6 @@ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "for live navigation"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( @@ -354,7 +342,6 @@ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "for live navigation"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme new file mode 100644 index 000000000..73a424593 --- /dev/null +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/example/Base.lproj/Main.storyboard b/Example/example/Base.lproj/Main.storyboard deleted file mode 100644 index 25a763858..000000000 --- a/Example/example/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/example/Info.plist b/Example/example/Info.plist index 4d5288dc9..2fead1990 100644 --- a/Example/example/Info.plist +++ b/Example/example/Info.plist @@ -17,8 +17,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index b3e21d8bf..902ceb96b 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -5,16 +5,57 @@ // Created by Patrick Kladek on 16.05.24. // +import MapboxCoreNavigation +import MapboxDirections +import MapboxNavigation +import MapLibre import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { + private let styleURL = Bundle.main.url(forResource: "Terrain", withExtension: "json")! // swiftlint:disable:this force_unwrapping + var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = (scene as? UIWindowScene) else { return } + + self.window = UIWindow(windowScene: windowScene) + let viewController = NavigationViewController() + viewController.mapView?.styleURL = self.styleURL + + let waypoints = [ + CLLocation(latitude: 52.032407, longitude: 5.580310), + CLLocation(latitude: 51.768686, longitude: 4.6827956) + ].map { Waypoint(location: $0) } + + viewController.mapView?.tracksUserCourse = false + viewController.mapView?.showsUserLocation = true + viewController.mapView?.zoomLevel = 12 + viewController.mapView?.centerCoordinate = waypoints[0].coordinate + + self.window?.rootViewController = viewController + self.window?.makeKeyAndVisible() + + return () + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { + let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) + options.shapeFormat = .polyline6 + options.distanceMeasurementSystem = .metric + options.attributeOptions = [] + + Directions.shared.calculate(options) { _, routes, _ in + guard let route = routes?.first else { return } + + let simulatedLocationManager = SimulatedLocationManager(route: route) + simulatedLocationManager.speedMultiplier = 2 + + viewController.start(with: route, locationManager: simulatedLocationManager) + } + } } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/Example/example/ViewController.swift b/Example/example/ViewController.swift index 6127e02e8..949fda05e 100644 --- a/Example/example/ViewController.swift +++ b/Example/example/ViewController.swift @@ -10,91 +10,61 @@ import MapboxDirections import MapboxNavigation import MapLibre -class ViewController: UIViewController { +class ViewController: NavigationViewController { private let styleURL = Bundle.main.url(forResource: "Terrain", withExtension: "json")! // swiftlint:disable:this force_unwrapping - var navigationView: NavigationMapView? - - // Keep `RouteController` in memory (class scope), - // otherwise location updates won't be triggered - public var mapboxRouteController: RouteController? - override func viewDidLoad() { super.viewDidLoad() - let navigationView = NavigationMapView(frame: .zero, styleURL: self.styleURL, config: MNConfig()) - self.navigationView = navigationView - self.view.addSubview(navigationView) - - navigationView.translatesAutoresizingMaskIntoConstraints = false - navigationView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - - let waypoints = [ - CLLocation(latitude: 52.032407, longitude: 5.580310), - CLLocation(latitude: 51.768686, longitude: 4.6827956) - ].map { Waypoint(location: $0) } - - navigationView.zoomLevel = 12 - navigationView.centerCoordinate = waypoints[0].coordinate - - let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) - options.shapeFormat = .polyline6 - options.distanceMeasurementSystem = .metric - options.attributeOptions = [] - - print("[\(type(of: self))] Calculating routes with URL: \(Directions.shared.url(forCalculating: options))") + self.mapView?.styleURL = self.styleURL + self.mapView?.reloadStyle(nil) - let viewController = NavigationViewController() - - Directions.shared.calculate(options) { _, routes, _ in - guard let route = routes?.first else { return } - - let simulatedLocationManager = SimulatedLocationManager(route: route) - simulatedLocationManager.speedMultiplier = 2 - - viewController.mapView?.styleURL = self.styleURL - self.present(viewController, animated: true) { - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { - viewController.begin(with: route, locationManager: simulatedLocationManager) - } - } - } - } -} - -// MARK: - RouteControllerDelegate - -extension ViewController: RouteControllerDelegate { - @objc - func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { - let camera = MLNMapCamera(lookingAtCenter: locations.first!.coordinate, acrossDistance: 500, pitch: 0, heading: 0) - - self.navigationView?.setCamera(camera, animated: true) - } - - @objc - func didPassVisualInstructionPoint(notification: NSNotification) { - guard let currentVisualInstruction = currentStepProgress(from: notification)?.currentVisualInstruction else { return } - - print(String( - format: "didPassVisualInstructionPoint primary text: %@ and secondary text: %@", - String(describing: currentVisualInstruction.primaryInstruction.text), - String(describing: currentVisualInstruction.secondaryInstruction?.text))) +// let navigationView = NavigationMapView(frame: .zero, styleURL: self.styleURL, config: MNConfig()) +// self.navigationView = navigationView +// self.view.addSubview(navigationView) +// +// navigationView.translatesAutoresizingMaskIntoConstraints = false +// navigationView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true +// navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true +// navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true +// navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true +// +// let waypoints = [ +// CLLocation(latitude: 52.032407, longitude: 5.580310), +// CLLocation(latitude: 51.768686, longitude: 4.6827956) +// ].map { Waypoint(location: $0) } +// +// navigationView.zoomLevel = 12 +// navigationView.centerCoordinate = waypoints[0].coordinate +// +// let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) +// options.shapeFormat = .polyline6 +// options.distanceMeasurementSystem = .metric +// options.attributeOptions = [] +// +// print("[\(type(of: self))] Calculating routes with URL: \(Directions.shared.url(forCalculating: options))") +// +// let viewController = NavigationViewController() +// +// Directions.shared.calculate(options) { _, routes, _ in +// guard let route = routes?.first else { return } +// +// let simulatedLocationManager = SimulatedLocationManager(route: route) +// simulatedLocationManager.speedMultiplier = 2 +// +// viewController.mapView?.styleURL = self.styleURL +// self.present(viewController, animated: true) { +// DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { +// viewController.begin(with: route, locationManager: simulatedLocationManager) +// } +// } +// } } - @objc - func didPassSpokenInstructionPoint(notification: NSNotification) { - guard let currentSpokenInstruction = currentStepProgress(from: notification)?.currentSpokenInstruction else { return } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.mapView?.styleURL = self.styleURL - print("didPassSpokenInstructionPoint text: \(currentSpokenInstruction.text)") - } - - private - func currentStepProgress(from notification: NSNotification) -> RouteStepProgress? { - let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress - return routeProgress?.currentLegProgress.currentStepProgress + self.mapView?.centerCoordinate = .init(latitude: 48.210033, longitude: 16.363449) } } diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index 007839f98..e347c6609 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -165,7 +165,7 @@ open class NavigationView: UIView { func commonInit() { self.setupViews() - setupConstraints() + self.setupConstraints() } func setupStackViews() { diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 02e064845..6d46df3a0 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -436,12 +436,16 @@ open class NavigationViewController: UIViewController { // MARK: - NavigationViewController - public func begin(with route: Route, locationManager: NavigationLocationManager? = nil) { + public func start(with route: Route, locationManager: NavigationLocationManager? = nil) { self.locationManager = locationManager self.route = route self.routeController?.resume() } + public func endRoute() { + // TODO: Dismiss + } + #if canImport(CarPlay) /** Presents a `NavigationViewController` on the top most view controller in the window and opens up the `StepsViewController`. diff --git a/MapboxNavigation/NavigationViewLayout.swift b/MapboxNavigation/NavigationViewLayout.swift index e180fcf5b..c0ef5c844 100644 --- a/MapboxNavigation/NavigationViewLayout.swift +++ b/MapboxNavigation/NavigationViewLayout.swift @@ -2,44 +2,45 @@ import UIKit extension NavigationView { func setupConstraints() { - mapView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mapView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mapView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - mapView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - instructionsBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - instructionsBannerContentView.bottomAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor).isActive = true - instructionsBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - instructionsBannerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - instructionsBannerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - instructionsBannerView.heightAnchor.constraint(equalToConstant: 96).isActive = true - + NSLayoutConstraint.activate([ + mapView.topAnchor.constraint(equalTo: topAnchor), + mapView.leadingAnchor.constraint(equalTo: leadingAnchor), + mapView.bottomAnchor.constraint(equalTo: bottomAnchor), + mapView.trailingAnchor.constraint(equalTo: trailingAnchor), + + instructionsBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor), + instructionsBannerContentView.bottomAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor), + instructionsBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor), + + instructionsBannerView.leadingAnchor.constraint(equalTo: leadingAnchor), + instructionsBannerView.trailingAnchor.constraint(equalTo: trailingAnchor), + instructionsBannerView.heightAnchor.constraint(equalToConstant: 96), + + informationStackView.topAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor), + informationStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + informationStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + + floatingStackView.topAnchor.constraint(equalTo: informationStackView.bottomAnchor, constant: 10), + floatingStackView.trailingAnchor.constraint(equalTo: safeTrailingAnchor, constant: -10), + + resumeButton.leadingAnchor.constraint(equalTo: safeLeadingAnchor, constant: 10), + resumeButton.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10), + + bottomBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor), + bottomBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor), + bottomBannerContentView.bottomAnchor.constraint(equalTo: bottomAnchor), + bottomBannerContentView.topAnchor.constraint(equalTo: bottomBannerView.topAnchor), + + bottomBannerView.trailingAnchor.constraint(equalTo: trailingAnchor), + bottomBannerView.leadingAnchor.constraint(equalTo: leadingAnchor), + bottomBannerView.bottomAnchor.constraint(equalTo: safeBottomAnchor), + + wayNameView.centerXAnchor.constraint(equalTo: centerXAnchor), + wayNameView.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10) + ]) NSLayoutConstraint.activate(bannerShowConstraints) - - informationStackView.topAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor).isActive = true - informationStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - informationStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - floatingStackView.topAnchor.constraint(equalTo: informationStackView.bottomAnchor, constant: 10).isActive = true - floatingStackView.trailingAnchor.constraint(equalTo: safeTrailingAnchor, constant: -10).isActive = true - - resumeButton.leadingAnchor.constraint(equalTo: safeLeadingAnchor, constant: 10).isActive = true - resumeButton.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10).isActive = true - - bottomBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - bottomBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - bottomBannerContentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - bottomBannerContentView.topAnchor.constraint(equalTo: bottomBannerView.topAnchor).isActive = true - - bottomBannerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - bottomBannerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - bottomBannerView.bottomAnchor.constraint(equalTo: safeBottomAnchor).isActive = true - - wayNameView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - wayNameView.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10).isActive = true } - + func constrainEndOfRoute() { endOfRouteHideConstraint?.isActive = true diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 2e0e488d0..bed490f78 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -9,6 +9,19 @@ class ArrowFillPolyline: MLNPolylineFeature {} class ArrowStrokePolyline: ArrowFillPolyline {} class RouteMapViewController: UIViewController { + typealias LabelRoadNameCompletionHandler = (_ defaultRaodNameAssigned: Bool) -> Void + + private enum Actions { + static let overview: Selector = #selector(RouteMapViewController.toggleOverview(_:)) + static let mute: Selector = #selector(RouteMapViewController.toggleMute(_:)) + static let recenter: Selector = #selector(RouteMapViewController.recenter(_:)) + } + + private lazy var geocoder: CLGeocoder = .init() + + // MARK: - Properties + + let distanceFormatter = DistanceFormatter(approximate: true) var navigationView: NavigationView { view as! NavigationView } var mapView: NavigationMapView { self.navigationView.mapView } var statusView: StatusView { self.navigationView.statusView } @@ -17,26 +30,13 @@ class RouteMapViewController: UIViewController { var nextBannerView: NextBannerView { self.navigationView.nextBannerView } var instructionsBannerView: InstructionsBannerView { self.navigationView.instructionsBannerView } var instructionsBannerContentView: InstructionsBannerContentView { self.navigationView.instructionsBannerContentView } - - lazy var endOfRouteViewController: EndOfRouteViewController = { - let storyboard = UIStoryboard(name: "Navigation", bundle: .mapboxNavigation) - let viewController = storyboard.instantiateViewController(withIdentifier: "EndOfRouteViewController") as! EndOfRouteViewController - return viewController - }() - - private enum Actions { - static let overview: Selector = #selector(RouteMapViewController.toggleOverview(_:)) - static let mute: Selector = #selector(RouteMapViewController.toggleMute(_:)) - static let recenter: Selector = #selector(RouteMapViewController.recenter(_:)) - } - var route: Route? { self.routeController?.routeProgress.route } var updateETATimer: Timer? var previewInstructionsView: StepInstructionsView? var lastTimeUserRerouted: Date? var stepsViewController: StepsViewController? - private lazy var geocoder: CLGeocoder = .init() var destination: Waypoint? + var showsEndOfRoute: Bool = true var isUsedInConjunctionWithCarPlayWindow = false { didSet { if self.isUsedInConjunctionWithCarPlayWindow { @@ -47,8 +47,6 @@ class RouteMapViewController: UIViewController { } } - var showsEndOfRoute: Bool = true - var pendingCamera: MLNMapCamera? { guard let parent = parent as? NavigationViewController else { return nil @@ -63,7 +61,6 @@ class RouteMapViewController: UIViewController { return camera } - weak var delegate: RouteMapViewControllerDelegate? var routeController: Router? { didSet { self.navigationView.statusView.canChangeValue = self.routeController?.locationManager is SimulatedLocationManager @@ -73,7 +70,7 @@ class RouteMapViewController: UIViewController { } } - let distanceFormatter = DistanceFormatter(approximate: true) + weak var delegate: RouteMapViewControllerDelegate? var arrowCurrentStep: RouteStep? var isInOverviewMode = false { didSet { @@ -92,20 +89,27 @@ class RouteMapViewController: UIViewController { var currentLegIndexMapped = 0 var currentStepIndexMapped = 0 - + var labelRoadNameCompletionHandler: LabelRoadNameCompletionHandler? /** A Boolean value that determines whether the map annotates the locations at which instructions are spoken for debugging purposes. */ var annotatesSpokenInstructions = false var overheadInsets: UIEdgeInsets { - UIEdgeInsets(top: self.navigationView.instructionsBannerView.bounds.height, left: 20, bottom: self.navigationView.bottomBannerView.bounds.height, right: 20) + UIEdgeInsets(top: self.navigationView.instructionsBannerView.bounds.height, + left: 20, + bottom: self.navigationView.bottomBannerView.bounds.height, + right: 20) } + + lazy var endOfRouteViewController: EndOfRouteViewController = { + let storyboard = UIStoryboard(name: "Navigation", bundle: .mapboxNavigation) + let viewController = storyboard.instantiateViewController(withIdentifier: "EndOfRouteViewController") as! EndOfRouteViewController + return viewController + }() - typealias LabelRoadNameCompletionHandler = (_ defaultRaodNameAssigned: Bool) -> Void - - var labelRoadNameCompletionHandler: LabelRoadNameCompletionHandler? - + // MARK: - Lifecycle + convenience init(routeController: RouteController?, delegate: RouteMapViewControllerDelegate? = nil) { self.init() self.routeController = routeController @@ -113,16 +117,23 @@ class RouteMapViewController: UIViewController { self.automaticallyAdjustsScrollViewInsets = false } + deinit { + self.suspendNotifications() + self.removeTimer() + } + + // MARK: - UIViewController + override func loadView() { - view = NavigationView(delegate: self) - view.frame = parent?.view.bounds ?? UIScreen.main.bounds + self.view = NavigationView(delegate: self) + self.view.frame = self.parent?.view.bounds ?? UIScreen.main.bounds } override func viewDidLoad() { super.viewDidLoad() self.mapView.contentInset = self.contentInsets - view.layoutIfNeeded() + self.view.layoutIfNeeded() self.mapView.tracksUserCourse = true @@ -135,15 +146,10 @@ class RouteMapViewController: UIViewController { self.notifyUserAboutLowVolume() } - deinit { - suspendNotifications() - removeTimer() - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - resetETATimer() + self.resetETATimer() self.navigationView.muteButton.isSelected = NavigationSettings.shared.voiceMuted self.mapView.compassView.isHidden = true @@ -167,7 +173,7 @@ class RouteMapViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.annotatesSpokenInstructions = self.delegate?.mapViewControllerShouldAnnotateSpokenInstructions(self) ?? false - showRouteIfNeeded() + self.showRouteIfNeeded() self.currentLegIndexMapped = self.routeController?.routeProgress.legIndex ?? 0 self.currentStepIndexMapped = self.routeController?.routeProgress.currentLegProgress.stepIndex ?? 0 } @@ -176,27 +182,7 @@ class RouteMapViewController: UIViewController { super.viewWillDisappear(animated) self.removeTimer() } - - func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(self.willReroute(notification:)), name: .routeControllerWillReroute, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didReroute(notification:)), name: .routeControllerDidReroute, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.rerouteDidFail(notification:)), name: .routeControllerDidFailToReroute, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.applicationWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.removeTimer), name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.updateInstructionsBanner(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: self.routeController) - subscribeToKeyboardNotifications() - } - - func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: .routeControllerWillReroute, object: nil) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidReroute, object: nil) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidFailToReroute, object: nil) - NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassVisualInstructionPoint, object: nil) - unsubscribeFromKeyboardNotifications() - } - + @objc func recenter(_ sender: AnyObject) { self.mapView.tracksUserCourse = true self.mapView.enableFrameByFrameCourseViewTracking(for: 3) @@ -367,17 +353,6 @@ class RouteMapViewController: UIViewController { } } - private func showStatus(title: String, withSpinner spin: Bool = false, for time: TimeInterval, animated: Bool = true, interactive: Bool = false) { - self.statusView.show(title, showSpinner: spin, interactive: interactive) - guard time < .infinity else { return } - self.statusView.hide(delay: time, animated: animated) - } - - private func setCamera(altitude: Double) { - guard self.mapView.altitude != altitude else { return } - self.mapView.altitude = altitude - } - func mapView(_ mapView: MLNMapView, imageFor annotation: MLNAnnotation) -> MLNAnnotationImage? { navigationMapView(mapView, imageFor: annotation) } @@ -504,15 +479,6 @@ class RouteMapViewController: UIViewController { guard duration > 0.0 else { return noAnimation() } UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: complete) } - - fileprivate func populateName(for waypoint: Waypoint, populated: @escaping (Waypoint) -> Void) { - guard waypoint.name == nil else { return populated(waypoint) } - CLGeocoder().reverseGeocodeLocation(waypoint.location) { places, error in - guard let place = places?.first, let placeName = place.name, error == nil else { return } - let named = Waypoint(coordinate: waypoint.coordinate, name: placeName) - return populated(named) - } - } } // MARK: - UIContentContainer @@ -960,6 +926,46 @@ private extension RouteMapViewController { UIView.animate(withDuration: options.duration, delay: 0, options: options.curve, animations: view.layoutIfNeeded, completion: nil) } + + func resumeNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(self.willReroute(notification:)), name: .routeControllerWillReroute, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.didReroute(notification:)), name: .routeControllerDidReroute, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.rerouteDidFail(notification:)), name: .routeControllerDidFailToReroute, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.applicationWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.removeTimer), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.updateInstructionsBanner(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: self.routeController) + self.subscribeToKeyboardNotifications() + } + + func suspendNotifications() { + NotificationCenter.default.removeObserver(self, name: .routeControllerWillReroute, object: nil) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidReroute, object: nil) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidFailToReroute, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassVisualInstructionPoint, object: nil) + self.unsubscribeFromKeyboardNotifications() + } + + func showStatus(title: String, withSpinner spin: Bool = false, for time: TimeInterval, animated: Bool = true, interactive: Bool = false) { + self.statusView.show(title, showSpinner: spin, interactive: interactive) + guard time < .infinity else { return } + self.statusView.hide(delay: time, animated: animated) + } + + func setCamera(altitude: Double) { + guard self.mapView.altitude != altitude else { return } + self.mapView.altitude = altitude + } + + func populateName(for waypoint: Waypoint, populated: @escaping (Waypoint) -> Void) { + guard waypoint.name == nil else { return populated(waypoint) } + CLGeocoder().reverseGeocodeLocation(waypoint.location) { places, error in + guard let place = places?.first, let placeName = place.name, error == nil else { return } + let named = Waypoint(coordinate: waypoint.coordinate, name: placeName) + return populated(named) + } + } } private extension UIView.AnimationOptions { From aa134820b3eb3796c6caa81ec82acb0ba5664be6 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Mon, 20 May 2024 11:41:53 +0200 Subject: [PATCH 06/44] hide UI and show when navigation starts --- Example/example/SceneDelegate.swift | 2 +- MapboxNavigation/BottomBannerViewLayout.swift | 12 +- .../NavigationViewController.swift | 12 +- MapboxNavigation/RouteMapViewController.swift | 723 +++++++++--------- 4 files changed, 383 insertions(+), 366 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index 902ceb96b..1547bf716 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -39,7 +39,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window?.rootViewController = viewController self.window?.makeKeyAndVisible() - return () +// return () DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) diff --git a/MapboxNavigation/BottomBannerViewLayout.swift b/MapboxNavigation/BottomBannerViewLayout.swift index 4a25fe583..7ca33ce43 100644 --- a/MapboxNavigation/BottomBannerViewLayout.swift +++ b/MapboxNavigation/BottomBannerViewLayout.swift @@ -5,34 +5,34 @@ extension BottomBannerView { let timeRemainingLabel = TimeRemainingLabel() timeRemainingLabel.translatesAutoresizingMaskIntoConstraints = false timeRemainingLabel.font = .systemFont(ofSize: 28, weight: .medium) - addSubview(timeRemainingLabel) + self.addSubview(timeRemainingLabel) self.timeRemainingLabel = timeRemainingLabel let distanceRemainingLabel = DistanceRemainingLabel() distanceRemainingLabel.translatesAutoresizingMaskIntoConstraints = false distanceRemainingLabel.font = .systemFont(ofSize: 18, weight: .medium) - addSubview(distanceRemainingLabel) + self.addSubview(distanceRemainingLabel) self.distanceRemainingLabel = distanceRemainingLabel let arrivalTimeLabel = ArrivalTimeLabel() arrivalTimeLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(arrivalTimeLabel) + self.addSubview(arrivalTimeLabel) self.arrivalTimeLabel = arrivalTimeLabel let cancelButton = CancelButton(type: .custom) cancelButton.translatesAutoresizingMaskIntoConstraints = false cancelButton.setImage(UIImage(named: "close", in: .mapboxNavigation, compatibleWith: nil), for: .normal) - addSubview(cancelButton) + self.addSubview(cancelButton) self.cancelButton = cancelButton let verticalDivider = SeparatorView() verticalDivider.translatesAutoresizingMaskIntoConstraints = false - addSubview(verticalDivider) + self.addSubview(verticalDivider) verticalDividerView = verticalDivider let horizontalDividerView = SeparatorView() horizontalDividerView.translatesAutoresizingMaskIntoConstraints = false - addSubview(horizontalDividerView) + self.addSubview(horizontalDividerView) self.horizontalDividerView = horizontalDividerView self.setupConstraints() diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 6d46df3a0..5c37cbde8 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -375,11 +375,16 @@ open class NavigationViewController: UIViewController { self.mapViewController = mapViewController mapViewController.destination = route?.legs.last?.destination mapViewController.willMove(toParent: self) - addChild(mapViewController) + self.addChild(mapViewController) mapViewController.didMove(toParent: self) let mapSubview: UIView = mapViewController.view mapSubview.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(mapSubview) + self.view.addSubview(mapSubview) + + if route == nil { + self.mapViewController?.navigationView.instructionsBannerContentView.isHidden = true + self.mapViewController?.navigationView.bottomBannerContentView.isHidden = true + } mapSubview.pinInSuperview() mapViewController.reportButton.isHidden = !self.showsReportFeedback @@ -440,6 +445,9 @@ open class NavigationViewController: UIViewController { self.locationManager = locationManager self.route = route self.routeController?.resume() + self.mapViewController?.navigationView.instructionsBannerContentView.isHidden = false + self.mapViewController?.navigationView.bottomBannerContentView.isHidden = false + self.mapViewController?.navigationView.bottomBannerView.traitCollectionDidChange(self.traitCollection) } public func endRoute() { diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index bed490f78..8b8c037b6 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -8,6 +8,22 @@ import UIKit class ArrowFillPolyline: MLNPolylineFeature {} class ArrowStrokePolyline: ArrowFillPolyline {} +@objc protocol RouteMapViewControllerDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, VisualInstructionDelegate { + func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) + func mapViewControllerShouldAnnotateSpokenInstructions(_ routeMapViewController: RouteMapViewController) -> Bool + + /** + Called to allow the delegate to customize the contents of the road name label that is displayed towards the bottom of the map view. + + This method is called on each location update. By default, the label displays the name of the road the user is currently traveling on. + + - parameter mapViewController: The route map view controller that will display the road name. + - parameter location: The user’s current location. + - return: The road name to display in the label, or the empty string to hide the label, or nil to query the map’s vector tiles for the road name. + */ + @objc func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? +} + class RouteMapViewController: UIViewController { typealias LabelRoadNameCompletionHandler = (_ defaultRaodNameAssigned: Bool) -> Void @@ -37,6 +53,14 @@ class RouteMapViewController: UIViewController { var stepsViewController: StepsViewController? var destination: Waypoint? var showsEndOfRoute: Bool = true + var arrowCurrentStep: RouteStep? + + var contentInsets: UIEdgeInsets { + let top = self.navigationView.instructionsBannerContentView.bounds.height + let bottom = self.navigationView.bottomBannerView.bounds.height + return UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0) + } + var isUsedInConjunctionWithCarPlayWindow = false { didSet { if self.isUsedInConjunctionWithCarPlayWindow { @@ -69,9 +93,7 @@ class RouteMapViewController: UIViewController { self.populateName(for: destination, populated: { self.destination = $0 }) } } - - weak var delegate: RouteMapViewControllerDelegate? - var arrowCurrentStep: RouteStep? + var isInOverviewMode = false { didSet { if self.isInOverviewMode { @@ -90,6 +112,7 @@ class RouteMapViewController: UIViewController { var currentLegIndexMapped = 0 var currentStepIndexMapped = 0 var labelRoadNameCompletionHandler: LabelRoadNameCompletionHandler? + /** A Boolean value that determines whether the map annotates the locations at which instructions are spoken for debugging purposes. */ @@ -101,13 +124,15 @@ class RouteMapViewController: UIViewController { bottom: self.navigationView.bottomBannerView.bounds.height, right: 20) } - + lazy var endOfRouteViewController: EndOfRouteViewController = { let storyboard = UIStoryboard(name: "Navigation", bundle: .mapboxNavigation) let viewController = storyboard.instantiateViewController(withIdentifier: "EndOfRouteViewController") as! EndOfRouteViewController return viewController }() + weak var delegate: RouteMapViewControllerDelegate? + // MARK: - Lifecycle convenience init(routeController: RouteController?, delegate: RouteMapViewControllerDelegate? = nil) { @@ -182,51 +207,6 @@ class RouteMapViewController: UIViewController { super.viewWillDisappear(animated) self.removeTimer() } - - @objc func recenter(_ sender: AnyObject) { - self.mapView.tracksUserCourse = true - self.mapView.enableFrameByFrameCourseViewTracking(for: 3) - self.isInOverviewMode = false - guard let routeController else { return } - - self.updateCameraAltitude(for: routeController.routeProgress) - - self.mapView.addArrow(route: routeController.routeProgress.route, - legIndex: routeController.routeProgress.legIndex, - stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) - - self.removePreviewInstructions() - } - - @objc func removeTimer() { - self.updateETATimer?.invalidate() - self.updateETATimer = nil - } - - func removePreviewInstructions() { - if let view = previewInstructionsView { - view.removeFromSuperview() - self.navigationView.instructionsBannerContentView.backgroundColor = InstructionsBannerView.appearance().backgroundColor - self.navigationView.instructionsBannerView.delegate = self - self.previewInstructionsView = nil - } - } - - @objc func toggleOverview(_ sender: Any) { - self.mapView.enableFrameByFrameCourseViewTracking(for: 3) - if let coordinates = self.routeController?.routeProgress.route.coordinates, - let userLocation = self.routeController?.locationManager.location?.coordinate { - self.mapView.setOverheadCameraView(from: userLocation, along: coordinates, for: self.overheadInsets) - } - self.isInOverviewMode = true - } - - @objc func toggleMute(_ sender: UIButton) { - sender.isSelected = !sender.isSelected - - let muted = sender.isSelected - NavigationSettings.shared.voiceMuted = muted - } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) @@ -238,7 +218,17 @@ class RouteMapViewController: UIViewController { self.mapView.setContentInset(self.contentInsets, animated: true, completionHandler: nil) self.mapView.setNeedsUpdateConstraints() } + + // MARK: - UIContentContainer + + override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { + self.navigationView.endOfRouteHeightConstraint?.constant = container.preferredContentSize.height + + UIView.animate(withDuration: 0.3, animations: view.layoutIfNeeded) + } + // MARK: - RouteMapViewController + func notifyDidReroute(route: Route) { self.updateETA() self.currentStepIndexMapped = 0 @@ -270,21 +260,6 @@ class RouteMapViewController: UIViewController { } } - @objc func applicationWillEnterForeground(notification: NSNotification) { - self.mapView.updateCourseTracking(location: self.routeController?.location, animated: false) - resetETATimer() - } - - @objc func willReroute(notification: NSNotification) { - let title = NSLocalizedString("REROUTING", bundle: .mapboxNavigation, value: "Rerouting…", comment: "Indicates that rerouting is in progress") - self.lanesView.hide() - self.statusView.show(title, showSpinner: true) - } - - @objc func rerouteDidFail(notification: NSNotification) { - self.statusView.hide() - } - func notifyUserAboutLowVolume() { guard !(self.routeController?.locationManager is SimulatedLocationManager) else { return } guard !NavigationSettings.shared.voiceMuted else { return } @@ -295,29 +270,6 @@ class RouteMapViewController: UIViewController { self.statusView.hide(delay: 3, animated: true) } - @objc func didReroute(notification: NSNotification) { - guard isViewLoaded else { return } - - if let locationManager = self.routeController?.locationManager as? SimulatedLocationManager { - let localized = String.Localized.simulationStatus(speed: Int(locationManager.speedMultiplier)) - self.showStatus(title: localized, for: .infinity, interactive: true) - } else { - self.statusView.hide(delay: 2, animated: true) - } - - if notification.userInfo![RouteControllerNotificationUserInfoKey.isProactiveKey] as! Bool { - let title = NSLocalizedString("FASTER_ROUTE_FOUND", bundle: .mapboxNavigation, value: "Faster Route Found", comment: "Indicates a faster route was found") - self.showStatus(title: title, withSpinner: true, for: 3) - } - } - - @objc func updateInstructionsBanner(notification: NSNotification) { - guard let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress else { return } - self.instructionsBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) - self.lanesView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) - self.nextBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) - } - func updateMapOverlays(for routeProgress: RouteProgress) { guard let routeController else { return } @@ -353,14 +305,6 @@ class RouteMapViewController: UIViewController { } } - func mapView(_ mapView: MLNMapView, imageFor annotation: MLNAnnotation) -> MLNAnnotationImage? { - navigationMapView(mapView, imageFor: annotation) - } - - func mapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? { - navigationMapView(mapView, viewFor: annotation) - } - func notifyDidChange(routeProgress: RouteProgress, location: CLLocation, secondsRemaining: TimeInterval) { resetETATimer() updateETA() @@ -384,12 +328,119 @@ class RouteMapViewController: UIViewController { } } - var contentInsets: UIEdgeInsets { - let top = self.navigationView.instructionsBannerContentView.bounds.height - let bottom = self.navigationView.bottomBannerView.bounds.height - return UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0) + /** + Updates the current road name label to reflect the road on which the user is currently traveling. + + - parameter location: The user’s current location. + */ + func labelCurrentRoad(at rawLocation: CLLocation, for snappedLoction: CLLocation? = nil) { + guard self.navigationView.resumeButton.isHidden else { + return + } + + let roadName = self.delegate?.mapViewController(self, roadNameAt: rawLocation) + guard roadName == nil else { + if let roadName { + self.navigationView.wayNameView.text = roadName + self.navigationView.wayNameView.isHidden = roadName.isEmpty + } + return + } + + let location = snappedLoction ?? rawLocation + + self.labelCurrentRoadFeature(at: location) + + if let labelRoadNameCompletionHandler { + labelRoadNameCompletionHandler(true) + } } + func labelCurrentRoadFeature(at location: CLLocation) { + guard let style = self.mapView.style, let stepCoordinates = self.routeController?.routeProgress.currentLegProgress.currentStep.coordinates else { + return + } + + let closestCoordinate = location.coordinate + let roadLabelLayerIdentifier = "roadLabelLayer" + var streetsSources: [MLNVectorTileSource] = style.sources.compactMap { + $0 as? MLNVectorTileSource + }.filter(\.isMapboxStreets) + + // Add Mapbox Streets if the map does not already have it + if streetsSources.isEmpty { + let source = MLNVectorTileSource(identifier: "mapboxStreetsv7", configurationURL: URL(string: "mapbox://mapbox.mapbox-streets-v7")!) + style.addSource(source) + streetsSources.append(source) + } + + if let mapboxSteetsSource = streetsSources.first, style.layer(withIdentifier: roadLabelLayerIdentifier) == nil { + let streetLabelLayer = MLNLineStyleLayer(identifier: roadLabelLayerIdentifier, source: mapboxSteetsSource) + streetLabelLayer.sourceLayerIdentifier = "road_label" + streetLabelLayer.lineOpacity = NSExpression(forConstantValue: 1) + streetLabelLayer.lineWidth = NSExpression(forConstantValue: 20) + streetLabelLayer.lineColor = NSExpression(forConstantValue: UIColor.white) + style.insertLayer(streetLabelLayer, at: 0) + } + + let userPuck = self.mapView.convert(closestCoordinate, toPointTo: self.mapView) + let features = self.mapView.visibleFeatures(at: userPuck, styleLayerIdentifiers: Set([roadLabelLayerIdentifier])) + var smallestLabelDistance = Double.infinity + var currentName: String? + var currentShieldName: NSAttributedString? + + for feature in features { + var allLines: [MLNPolyline] = [] + + if let line = feature as? MLNPolylineFeature { + allLines.append(line) + } else if let lines = feature as? MLNMultiPolylineFeature { + allLines = lines.polylines + } + + for line in allLines { + let featureCoordinates = Array(UnsafeBufferPointer(start: line.coordinates, count: Int(line.pointCount))) + let featurePolyline = Polyline(featureCoordinates) + let slicedLine = Polyline(stepCoordinates).sliced(from: closestCoordinate) + + let lookAheadDistance: CLLocationDistance = 10 + guard let pointAheadFeature = featurePolyline.sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } + guard let pointAheadUser = slicedLine.coordinateFromStart(distance: lookAheadDistance) else { continue } + guard let reversedPoint = Polyline(featureCoordinates.reversed()).sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } + + let distanceBetweenPointsAhead = pointAheadFeature.distance(to: pointAheadUser) + let distanceBetweenReversedPoint = reversedPoint.distance(to: pointAheadUser) + let minDistanceBetweenPoints = min(distanceBetweenPointsAhead, distanceBetweenReversedPoint) + + if minDistanceBetweenPoints < smallestLabelDistance { + smallestLabelDistance = minDistanceBetweenPoints + + if let line = feature as? MLNPolylineFeature { + let roadNameRecord = self.roadFeature(for: line) + currentShieldName = roadNameRecord.shieldName + currentName = roadNameRecord.roadName + } else if let line = feature as? MLNMultiPolylineFeature { + let roadNameRecord = self.roadFeature(for: line) + currentShieldName = roadNameRecord.shieldName + currentName = roadNameRecord.roadName + } + } + } + } + + let hasWayName = currentName != nil || currentShieldName != nil + if smallestLabelDistance < 5, hasWayName { + if let currentShieldName { + self.navigationView.wayNameView.attributedText = currentShieldName + } else if let currentName { + self.navigationView.wayNameView.text = currentName + } + self.navigationView.wayNameView.isHidden = false + } else { + self.navigationView.wayNameView.isHidden = true + } + } + // MARK: End Of Route func embedEndOfRoute() { @@ -481,16 +532,6 @@ class RouteMapViewController: UIViewController { } } -// MARK: - UIContentContainer - -extension RouteMapViewController { - override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - self.navigationView.endOfRouteHeightConstraint?.constant = container.preferredContentSize.height - - UIView.animate(withDuration: 0.3, animations: view.layoutIfNeeded) - } -} - // MARK: - NavigationViewDelegate extension RouteMapViewController: NavigationViewDelegate { @@ -524,6 +565,8 @@ extension RouteMapViewController: NavigationViewDelegate { self.delegate?.mapViewDidFinishLoadingMap?(mapView) } + // MARK: - VisualInstructionDelegate + func label(_ label: InstructionLabel, willPresent instruction: VisualInstruction, as presented: NSAttributedString) -> NSAttributedString? { self.delegate?.label?(label, willPresent: instruction, as: presented) } @@ -550,33 +593,8 @@ extension RouteMapViewController: NavigationViewDelegate { func didTapInstructionsBanner(_ sender: BaseInstructionsBannerView) { self.displayPreviewInstructions() } - - private func displayPreviewInstructions() { - self.removePreviewInstructions() - if let controller = stepsViewController { - self.stepsViewController = nil - controller.dismiss() - } else if let routeController { - let controller = StepsViewController(routeProgress: routeController.routeProgress) - controller.delegate = self - addChild(controller) - view.insertSubview(controller.view, belowSubview: self.navigationView.instructionsBannerContentView) - - controller.view.topAnchor.constraint(equalTo: self.navigationView.instructionsBannerContentView.bottomAnchor).isActive = true - controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - - controller.didMove(toParent: self) - controller.dropDownAnimation() - - self.stepsViewController = controller - return - } - } - - // MARK: NavigationMapViewDelegate + // MARK: NavigationMapViewDelegate func navigationMapView(_ mapView: NavigationMapView, routeStyleLayerWithIdentifier identifier: String, source: MLNSource) -> MLNStyleLayer? { self.delegate?.navigationMapView?(mapView, routeStyleLayerWithIdentifier: identifier, source: source) @@ -627,200 +645,6 @@ extension RouteMapViewController: NavigationViewDelegate { // otherwise, ask the delegate or return .zero return self.delegate?.navigationMapViewUserAnchorPoint?(mapView) ?? .zero } - - /** - Updates the current road name label to reflect the road on which the user is currently traveling. - - - parameter location: The user’s current location. - */ - func labelCurrentRoad(at rawLocation: CLLocation, for snappedLoction: CLLocation? = nil) { - guard self.navigationView.resumeButton.isHidden else { - return - } - - let roadName = self.delegate?.mapViewController(self, roadNameAt: rawLocation) - guard roadName == nil else { - if let roadName { - self.navigationView.wayNameView.text = roadName - self.navigationView.wayNameView.isHidden = roadName.isEmpty - } - return - } - - let location = snappedLoction ?? rawLocation - - self.labelCurrentRoadFeature(at: location) - - if let labelRoadNameCompletionHandler { - labelRoadNameCompletionHandler(true) - } - } - - func labelCurrentRoadFeature(at location: CLLocation) { - guard let style = self.mapView.style, let stepCoordinates = self.routeController?.routeProgress.currentLegProgress.currentStep.coordinates else { - return - } - - let closestCoordinate = location.coordinate - let roadLabelLayerIdentifier = "roadLabelLayer" - var streetsSources: [MLNVectorTileSource] = style.sources.compactMap { - $0 as? MLNVectorTileSource - }.filter(\.isMapboxStreets) - - // Add Mapbox Streets if the map does not already have it - if streetsSources.isEmpty { - let source = MLNVectorTileSource(identifier: "mapboxStreetsv7", configurationURL: URL(string: "mapbox://mapbox.mapbox-streets-v7")!) - style.addSource(source) - streetsSources.append(source) - } - - if let mapboxSteetsSource = streetsSources.first, style.layer(withIdentifier: roadLabelLayerIdentifier) == nil { - let streetLabelLayer = MLNLineStyleLayer(identifier: roadLabelLayerIdentifier, source: mapboxSteetsSource) - streetLabelLayer.sourceLayerIdentifier = "road_label" - streetLabelLayer.lineOpacity = NSExpression(forConstantValue: 1) - streetLabelLayer.lineWidth = NSExpression(forConstantValue: 20) - streetLabelLayer.lineColor = NSExpression(forConstantValue: UIColor.white) - style.insertLayer(streetLabelLayer, at: 0) - } - - let userPuck = self.mapView.convert(closestCoordinate, toPointTo: self.mapView) - let features = self.mapView.visibleFeatures(at: userPuck, styleLayerIdentifiers: Set([roadLabelLayerIdentifier])) - var smallestLabelDistance = Double.infinity - var currentName: String? - var currentShieldName: NSAttributedString? - - for feature in features { - var allLines: [MLNPolyline] = [] - - if let line = feature as? MLNPolylineFeature { - allLines.append(line) - } else if let lines = feature as? MLNMultiPolylineFeature { - allLines = lines.polylines - } - - for line in allLines { - let featureCoordinates = Array(UnsafeBufferPointer(start: line.coordinates, count: Int(line.pointCount))) - let featurePolyline = Polyline(featureCoordinates) - let slicedLine = Polyline(stepCoordinates).sliced(from: closestCoordinate) - - let lookAheadDistance: CLLocationDistance = 10 - guard let pointAheadFeature = featurePolyline.sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } - guard let pointAheadUser = slicedLine.coordinateFromStart(distance: lookAheadDistance) else { continue } - guard let reversedPoint = Polyline(featureCoordinates.reversed()).sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } - - let distanceBetweenPointsAhead = pointAheadFeature.distance(to: pointAheadUser) - let distanceBetweenReversedPoint = reversedPoint.distance(to: pointAheadUser) - let minDistanceBetweenPoints = min(distanceBetweenPointsAhead, distanceBetweenReversedPoint) - - if minDistanceBetweenPoints < smallestLabelDistance { - smallestLabelDistance = minDistanceBetweenPoints - - if let line = feature as? MLNPolylineFeature { - let roadNameRecord = self.roadFeature(for: line) - currentShieldName = roadNameRecord.shieldName - currentName = roadNameRecord.roadName - } else if let line = feature as? MLNMultiPolylineFeature { - let roadNameRecord = self.roadFeature(for: line) - currentShieldName = roadNameRecord.shieldName - currentName = roadNameRecord.roadName - } - } - } - } - - let hasWayName = currentName != nil || currentShieldName != nil - if smallestLabelDistance < 5, hasWayName { - if let currentShieldName { - self.navigationView.wayNameView.attributedText = currentShieldName - } else if let currentName { - self.navigationView.wayNameView.text = currentName - } - self.navigationView.wayNameView.isHidden = false - } else { - self.navigationView.wayNameView.isHidden = true - } - } - - private func roadFeature(for line: MLNPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { - let roadNameRecord = self.roadFeatureHelper(ref: line.attribute(forKey: "ref"), - shield: line.attribute(forKey: "shield"), - reflen: line.attribute(forKey: "reflen"), - name: line.attribute(forKey: "name")) - - return (roadName: roadNameRecord.roadName, shieldName: roadNameRecord.shieldName) - } - - private func roadFeature(for line: MLNMultiPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { - let roadNameRecord = self.roadFeatureHelper(ref: line.attribute(forKey: "ref"), - shield: line.attribute(forKey: "shield"), - reflen: line.attribute(forKey: "reflen"), - name: line.attribute(forKey: "name")) - - return (roadName: roadNameRecord.roadName, shieldName: roadNameRecord.shieldName) - } - - private func roadFeatureHelper(ref: Any?, shield: Any?, reflen: Any?, name: Any?) -> (roadName: String?, shieldName: NSAttributedString?) { - var currentShieldName: NSAttributedString?, currentRoadName: String? - - if let text = ref as? String, let shieldID = shield as? String, let reflenDigit = reflen as? Int { - currentShieldName = self.roadShieldName(for: text, shield: shieldID, reflen: reflenDigit) - } - - if let roadName = name as? String { - currentRoadName = roadName - } - - if let compositeShieldImage = currentShieldName, let roadName = currentRoadName { - let compositeShield = NSMutableAttributedString(string: " \(roadName)") - compositeShield.insert(compositeShieldImage, at: 0) - currentShieldName = compositeShield - } - - return (roadName: currentRoadName, shieldName: currentShieldName) - } - - private func roadShieldName(for text: String?, shield: String?, reflen: Int?) -> NSAttributedString? { - guard let text, let shield, let reflen else { return nil } - - let currentShield = HighwayShield.RoadType(rawValue: shield) - let textColor = currentShield?.textColor ?? .black - let imageName = "\(shield)-\(reflen)" - - guard let image = mapView.style?.image(forName: imageName) else { - return nil - } - - let attachment = RoadNameLabelAttachment(image: image, text: text, color: textColor, font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize), scale: UIScreen.main.scale) - return NSAttributedString(attachment: attachment) - } - - @objc func updateETA() { - guard isViewLoaded, let routeController else { return } - - self.navigationView.bottomBannerView.updateETA(routeProgress: routeController.routeProgress) - } - - func resetETATimer() { - self.removeTimer() - self.updateETATimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(self.updateETA), userInfo: nil, repeats: true) - } - - func showRouteIfNeeded() { - guard self.isViewLoaded, self.view.window != nil else { return } - guard !self.mapView.showsRoute else { return } - guard let routeController else { return } - - self.mapView.showRoutes([routeController.routeProgress.route], legIndex: routeController.routeProgress.legIndex) - self.mapView.showWaypoints(routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex) - - if routeController.routeProgress.currentLegProgress.stepIndex + 1 <= routeController.routeProgress.currentLegProgress.leg.steps.count { - self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) - } - - if self.annotatesSpokenInstructions { - self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) - } - } } // MARK: StepsViewControllerDelegate @@ -884,17 +708,8 @@ extension RouteMapViewController: StepsViewControllerDelegate { // MARK: - Keyboard Handling private extension RouteMapViewController { - func subscribeToKeyboardNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - func unsubscribeFromKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @objc func keyboardWillShow(notification: NSNotification) { + @objc + func keyboardWillShow(notification: NSNotification) { guard self.navigationView.endOfRouteView != nil else { return } guard let userInfo = notification.userInfo else { return } guard let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int else { return } @@ -915,7 +730,8 @@ private extension RouteMapViewController { UIView.animate(withDuration: options.duration, delay: 0, options: opts, animations: view.layoutIfNeeded, completion: nil) } - @objc func keyboardWillHide(notification: NSNotification) { + @objc + func keyboardWillHide(notification: NSNotification) { guard self.navigationView.endOfRouteView != nil else { return } guard let userInfo = notification.userInfo else { return } let curve = UIView.AnimationCurve(rawValue: userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int) @@ -927,6 +743,106 @@ private extension RouteMapViewController { UIView.animate(withDuration: options.duration, delay: 0, options: options.curve, animations: view.layoutIfNeeded, completion: nil) } + @objc + func recenter(_ sender: AnyObject) { + self.mapView.tracksUserCourse = true + self.mapView.enableFrameByFrameCourseViewTracking(for: 3) + self.isInOverviewMode = false + guard let routeController else { return } + + self.updateCameraAltitude(for: routeController.routeProgress) + + self.mapView.addArrow(route: routeController.routeProgress.route, + legIndex: routeController.routeProgress.legIndex, + stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) + + self.removePreviewInstructions() + } + + @objc + func removeTimer() { + self.updateETATimer?.invalidate() + self.updateETATimer = nil + } + + @objc + func toggleOverview(_ sender: Any) { + self.mapView.enableFrameByFrameCourseViewTracking(for: 3) + if let coordinates = self.routeController?.routeProgress.route.coordinates, + let userLocation = self.routeController?.locationManager.location?.coordinate { + self.mapView.setOverheadCameraView(from: userLocation, along: coordinates, for: self.overheadInsets) + } + self.isInOverviewMode = true + } + + @objc + func toggleMute(_ sender: UIButton) { + sender.isSelected = !sender.isSelected + + let muted = sender.isSelected + NavigationSettings.shared.voiceMuted = muted + } + + @objc + func applicationWillEnterForeground(notification: NSNotification) { + self.mapView.updateCourseTracking(location: self.routeController?.location, animated: false) + self.resetETATimer() + } + + @objc + func willReroute(notification: NSNotification) { + let title = NSLocalizedString("REROUTING", bundle: .mapboxNavigation, value: "Rerouting…", comment: "Indicates that rerouting is in progress") + self.lanesView.hide() + self.statusView.show(title, showSpinner: true) + } + + @objc + func rerouteDidFail(notification: NSNotification) { + self.statusView.hide() + } + + @objc + func didReroute(notification: NSNotification) { + guard isViewLoaded else { return } + + if let locationManager = self.routeController?.locationManager as? SimulatedLocationManager { + let localized = String.Localized.simulationStatus(speed: Int(locationManager.speedMultiplier)) + self.showStatus(title: localized, for: .infinity, interactive: true) + } else { + self.statusView.hide(delay: 2, animated: true) + } + + if notification.userInfo![RouteControllerNotificationUserInfoKey.isProactiveKey] as! Bool { + let title = NSLocalizedString("FASTER_ROUTE_FOUND", bundle: .mapboxNavigation, value: "Faster Route Found", comment: "Indicates a faster route was found") + self.showStatus(title: title, withSpinner: true, for: 3) + } + } + + @objc + func updateInstructionsBanner(notification: NSNotification) { + guard let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress else { return } + self.instructionsBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) + self.lanesView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) + self.nextBannerView.update(for: routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction) + } + + @objc + func updateETA() { + guard isViewLoaded, let routeController else { return } + + self.navigationView.bottomBannerView.updateETA(routeProgress: routeController.routeProgress) + } + + func subscribeToKeyboardNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + func unsubscribeFromKeyboardNotifications() { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + func resumeNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(self.willReroute(notification:)), name: .routeControllerWillReroute, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.didReroute(notification:)), name: .routeControllerDidReroute, object: nil) @@ -966,6 +882,115 @@ private extension RouteMapViewController { return populated(named) } } + + func removePreviewInstructions() { + if let view = previewInstructionsView { + view.removeFromSuperview() + self.navigationView.instructionsBannerContentView.backgroundColor = InstructionsBannerView.appearance().backgroundColor + self.navigationView.instructionsBannerView.delegate = self + self.previewInstructionsView = nil + } + } + + func displayPreviewInstructions() { + self.removePreviewInstructions() + + if let controller = stepsViewController { + self.stepsViewController = nil + controller.dismiss() + } else if let routeController { + let controller = StepsViewController(routeProgress: routeController.routeProgress) + controller.delegate = self + addChild(controller) + view.insertSubview(controller.view, belowSubview: self.navigationView.instructionsBannerContentView) + + controller.view.topAnchor.constraint(equalTo: self.navigationView.instructionsBannerContentView.bottomAnchor).isActive = true + controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + controller.didMove(toParent: self) + controller.dropDownAnimation() + + self.stepsViewController = controller + return + } + } + + func roadFeature(for line: MLNPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { + let roadNameRecord = self.roadFeatureHelper(ref: line.attribute(forKey: "ref"), + shield: line.attribute(forKey: "shield"), + reflen: line.attribute(forKey: "reflen"), + name: line.attribute(forKey: "name")) + + return (roadName: roadNameRecord.roadName, shieldName: roadNameRecord.shieldName) + } + + func roadFeature(for line: MLNMultiPolylineFeature) -> (roadName: String?, shieldName: NSAttributedString?) { + let roadNameRecord = self.roadFeatureHelper(ref: line.attribute(forKey: "ref"), + shield: line.attribute(forKey: "shield"), + reflen: line.attribute(forKey: "reflen"), + name: line.attribute(forKey: "name")) + + return (roadName: roadNameRecord.roadName, shieldName: roadNameRecord.shieldName) + } + + func roadFeatureHelper(ref: Any?, shield: Any?, reflen: Any?, name: Any?) -> (roadName: String?, shieldName: NSAttributedString?) { + var currentShieldName: NSAttributedString?, currentRoadName: String? + + if let text = ref as? String, let shieldID = shield as? String, let reflenDigit = reflen as? Int { + currentShieldName = self.roadShieldName(for: text, shield: shieldID, reflen: reflenDigit) + } + + if let roadName = name as? String { + currentRoadName = roadName + } + + if let compositeShieldImage = currentShieldName, let roadName = currentRoadName { + let compositeShield = NSMutableAttributedString(string: " \(roadName)") + compositeShield.insert(compositeShieldImage, at: 0) + currentShieldName = compositeShield + } + + return (roadName: currentRoadName, shieldName: currentShieldName) + } + + func roadShieldName(for text: String?, shield: String?, reflen: Int?) -> NSAttributedString? { + guard let text, let shield, let reflen else { return nil } + + let currentShield = HighwayShield.RoadType(rawValue: shield) + let textColor = currentShield?.textColor ?? .black + let imageName = "\(shield)-\(reflen)" + + guard let image = mapView.style?.image(forName: imageName) else { + return nil + } + + let attachment = RoadNameLabelAttachment(image: image, text: text, color: textColor, font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize), scale: UIScreen.main.scale) + return NSAttributedString(attachment: attachment) + } + + func resetETATimer() { + self.removeTimer() + self.updateETATimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(self.updateETA), userInfo: nil, repeats: true) + } + + func showRouteIfNeeded() { + guard self.isViewLoaded, self.view.window != nil else { return } + guard !self.mapView.showsRoute else { return } + guard let routeController else { return } + + self.mapView.showRoutes([routeController.routeProgress.route], legIndex: routeController.routeProgress.legIndex) + self.mapView.showWaypoints(routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex) + + if routeController.routeProgress.currentLegProgress.stepIndex + 1 <= routeController.routeProgress.currentLegProgress.leg.steps.count { + self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) + } + + if self.annotatesSpokenInstructions { + self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) + } + } } private extension UIView.AnimationOptions { @@ -984,19 +1009,3 @@ private extension UIView.AnimationOptions { } } } - -@objc protocol RouteMapViewControllerDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, VisualInstructionDelegate { - func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) - func mapViewControllerShouldAnnotateSpokenInstructions(_ routeMapViewController: RouteMapViewController) -> Bool - - /** - Called to allow the delegate to customize the contents of the road name label that is displayed towards the bottom of the map view. - - This method is called on each location update. By default, the label displays the name of the road the user is currently traveling on. - - - parameter mapViewController: The route map view controller that will display the road name. - - parameter location: The user’s current location. - - return: The road name to display in the label, or the empty string to hide the label, or nil to query the map’s vector tiles for the road name. - */ - @objc func mapViewController(_ mapViewController: RouteMapViewController, roadNameAt location: CLLocation) -> String? -} From 202611d51c404311348d07a1526d7150379d30e1 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Mon, 20 May 2024 12:05:10 +0200 Subject: [PATCH 07/44] hide UI on route canceled --- MapboxCoreNavigation/RouteController.swift | 6 ++-- .../NavigationViewController.swift | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index 15b0b1ff6..f1c37e1ad 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -148,14 +148,14 @@ open class RouteController: NSObject, Router { self.locationManager.delegate = self self.resumeNotifications() - checkForUpdates() - checkForLocationUsageDescription() + self.checkForUpdates() + self.checkForLocationUsageDescription() self.tunnelIntersectionManager.delegate = self } deinit { - endNavigation() + self.endNavigation() guard let shouldDisable = delegate?.routeControllerShouldDisableBatteryMonitoring?(self) else { UIDevice.current.isBatteryMonitoringEnabled = false diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 5c37cbde8..0416e0719 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -450,8 +450,32 @@ open class NavigationViewController: UIViewController { self.mapViewController?.navigationView.bottomBannerView.traitCollectionDidChange(self.traitCollection) } - public func endRoute() { - // TODO: Dismiss + public func endRoute(animated: Bool = true) { + let route = self.route! + + self.routeController?.endNavigation() + self.mapView?.removeRoutes() + self.voiceController = nil + self.route = nil + + UIView.animate(withDuration: CATransaction.animationDuration()) { + self.mapViewController?.navigationView.instructionsBannerContentView.alpha = 0 + self.mapViewController?.navigationView.lanesView.alpha = 0 + self.mapViewController?.navigationView.bottomBannerContentView.alpha = 0 + } completion: { _ in + self.mapViewController?.navigationView.instructionsBannerContentView.isHidden = true + self.mapViewController?.navigationView.lanesView.isHidden = true + self.mapViewController?.navigationView.bottomBannerContentView.isHidden = true + self.mapViewController?.navigationView.bottomBannerView.traitCollectionDidChange(self.traitCollection) + + self.mapViewController?.navigationView.instructionsBannerContentView.alpha = 1 + self.mapViewController?.navigationView.lanesView.alpha = 1 + self.mapViewController?.navigationView.bottomBannerContentView.alpha = 1 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { + self.start(with: route, locationManager: SimulatedLocationManager(route: route)) + } } #if canImport(CarPlay) @@ -537,11 +561,7 @@ extension NavigationViewController: RouteMapViewControllerDelegate { } func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { - if self.delegate?.navigationViewControllerDidDismiss?(self, byCanceling: canceled) != nil { - // The receiver should handle dismissal of the NavigationViewController - } else { - dismiss(animated: true, completion: nil) - } + self.endRoute() } public func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { From ef43ec6ad535b4baed4c22fce8848415bfaf489b Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Mon, 20 May 2024 12:12:25 +0200 Subject: [PATCH 08/44] correctly hide and show Controls with animation --- MapboxNavigation/NavigationView.swift | 87 +++++++++++++------ .../NavigationViewController.swift | 23 +---- 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index e347c6609..fcf1502da 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -2,6 +2,10 @@ import MapboxDirections import MapLibre import UIKit +protocol NavigationViewDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, StatusViewDelegate, InstructionsBannerViewDelegate, NavigationMapViewCourseTrackingDelegate, VisualInstructionDelegate { + func navigationView(_ view: NavigationView, didTapCancelButton: CancelButton) +} + /** A view that represents the root view of the MapboxNavigation drop-in UI. @@ -145,7 +149,7 @@ open class NavigationView: UIView { } } - // MARK: - Initializers + // MARK: - Lifecycle convenience init(delegate: NavigationViewDelegate) { self.init(frame: .zero) @@ -163,26 +167,74 @@ open class NavigationView: UIView { self.commonInit() } + // MARK: - NavigationView + + override open func prepareForInterfaceBuilder() { + super.prepareForInterfaceBuilder() + DayStyle().apply() + [self.mapView, self.instructionsBannerView, self.lanesView, self.bottomBannerView, self.nextBannerView].forEach { $0.prepareForInterfaceBuilder() } + self.wayNameView.text = "Street Label" + } + + func showUI(animated: Bool = true) { + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { + self.instructionsBannerContentView.alpha = 1 + self.lanesView.alpha = 1 + self.bottomBannerContentView.alpha = 1 + self.floatingStackView.alpha = 1 + } completion: { _ in + self.instructionsBannerContentView.isHidden = false + self.lanesView.isHidden = false + self.bottomBannerContentView.isHidden = false + self.floatingStackView.isHidden = false + self.bottomBannerView.traitCollectionDidChange(self.traitCollection) + } + } + + func hideUI(animated: Bool = true) { + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { + self.instructionsBannerContentView.alpha = 0 + self.lanesView.alpha = 0 + self.bottomBannerContentView.alpha = 0 + self.floatingStackView.alpha = 0 + } completion: { _ in + self.instructionsBannerContentView.isHidden = true + self.lanesView.isHidden = true + self.bottomBannerContentView.isHidden = true + self.floatingStackView.isHidden = true + self.bottomBannerView.traitCollectionDidChange(self.traitCollection) + } + } +} + +// MARK: - Private + +private extension NavigationView { + @objc + func cancelButtonTapped(_ sender: CancelButton) { + self.delegate?.navigationView(self, didTapCancelButton: self.bottomBannerView.cancelButton) + } + func commonInit() { self.setupViews() self.setupConstraints() } - + func setupStackViews() { self.setupInformationStackView() self.floatingStackView.addArrangedSubviews([self.overviewButton, self.muteButton, self.reportButton]) } - + func setupInformationStackView() { let informationChildren: [UIView] = [instructionsBannerView, lanesView, nextBannerView, statusView] self.informationStackView.addArrangedSubviews(informationChildren) - + for informationChild in informationChildren { informationChild.leadingAnchor.constraint(equalTo: self.informationStackView.leadingAnchor).isActive = true informationChild.trailingAnchor.constraint(equalTo: self.informationStackView.trailingAnchor).isActive = true } } - + func setupContainers() { let containers: [(UIView, UIView)] = [ (instructionsBannerContentView, instructionsBannerView), @@ -190,11 +242,11 @@ open class NavigationView: UIView { ] containers.forEach { $0.addSubview($1) } } - + func setupViews() { self.setupStackViews() self.setupContainers() - + let subviews: [UIView] = [ mapView, informationStackView, @@ -204,22 +256,11 @@ open class NavigationView: UIView { bottomBannerContentView, instructionsBannerContentView ] - + subviews.forEach(addSubview(_:)) } - - override open func prepareForInterfaceBuilder() { - super.prepareForInterfaceBuilder() - DayStyle().apply() - [self.mapView, self.instructionsBannerView, self.lanesView, self.bottomBannerView, self.nextBannerView].forEach { $0.prepareForInterfaceBuilder() } - self.wayNameView.text = "Street Label" - } - - @objc func cancelButtonTapped(_ sender: CancelButton) { - self.delegate?.navigationView(self, didTapCancelButton: self.bottomBannerView.cancelButton) - } - - private func updateDelegates() { + + func updateDelegates() { self.mapView.delegate = self.delegate self.mapView.navigationMapDelegate = self.delegate self.mapView.courseTrackingDelegate = self.delegate @@ -229,7 +270,3 @@ open class NavigationView: UIView { self.statusView.delegate = self.delegate } } - -protocol NavigationViewDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, StatusViewDelegate, InstructionsBannerViewDelegate, NavigationMapViewCourseTrackingDelegate, VisualInstructionDelegate { - func navigationView(_ view: NavigationView, didTapCancelButton: CancelButton) -} diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 0416e0719..ed5c794c3 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -382,8 +382,7 @@ open class NavigationViewController: UIViewController { self.view.addSubview(mapSubview) if route == nil { - self.mapViewController?.navigationView.instructionsBannerContentView.isHidden = true - self.mapViewController?.navigationView.bottomBannerContentView.isHidden = true + self.mapViewController?.navigationView.hideUI(animated: false) } mapSubview.pinInSuperview() @@ -445,9 +444,8 @@ open class NavigationViewController: UIViewController { self.locationManager = locationManager self.route = route self.routeController?.resume() - self.mapViewController?.navigationView.instructionsBannerContentView.isHidden = false - self.mapViewController?.navigationView.bottomBannerContentView.isHidden = false - self.mapViewController?.navigationView.bottomBannerView.traitCollectionDidChange(self.traitCollection) + + self.mapViewController?.navigationView.showUI(animated: true) } public func endRoute(animated: Bool = true) { @@ -458,20 +456,7 @@ open class NavigationViewController: UIViewController { self.voiceController = nil self.route = nil - UIView.animate(withDuration: CATransaction.animationDuration()) { - self.mapViewController?.navigationView.instructionsBannerContentView.alpha = 0 - self.mapViewController?.navigationView.lanesView.alpha = 0 - self.mapViewController?.navigationView.bottomBannerContentView.alpha = 0 - } completion: { _ in - self.mapViewController?.navigationView.instructionsBannerContentView.isHidden = true - self.mapViewController?.navigationView.lanesView.isHidden = true - self.mapViewController?.navigationView.bottomBannerContentView.isHidden = true - self.mapViewController?.navigationView.bottomBannerView.traitCollectionDidChange(self.traitCollection) - - self.mapViewController?.navigationView.instructionsBannerContentView.alpha = 1 - self.mapViewController?.navigationView.lanesView.alpha = 1 - self.mapViewController?.navigationView.bottomBannerContentView.alpha = 1 - } + self.mapViewController?.navigationView.hideUI(animated: animated) DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { self.start(with: route, locationManager: SimulatedLocationManager(route: route)) From 22b9a3034e15b8ee790c6159168ea30869e635b2 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 21 May 2024 16:13:49 +0200 Subject: [PATCH 09/44] hide course view when no view is assigned --- MapboxNavigation/NavigationViewController.swift | 1 + MapboxNavigation/RouteMapViewController.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index ed5c794c3..5a20333d1 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -383,6 +383,7 @@ open class NavigationViewController: UIViewController { if route == nil { self.mapViewController?.navigationView.hideUI(animated: false) + self.mapView?.tracksUserCourse = false } mapSubview.pinInSuperview() diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 8b8c037b6..dc69de1b1 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -160,7 +160,7 @@ class RouteMapViewController: UIViewController { self.mapView.contentInset = self.contentInsets self.view.layoutIfNeeded() - self.mapView.tracksUserCourse = true + self.mapView.tracksUserCourse = self.route != nil self.distanceFormatter.numberFormatter.locale = .nationalizedCurrent @@ -179,7 +179,7 @@ class RouteMapViewController: UIViewController { self.navigationView.muteButton.isSelected = NavigationSettings.shared.voiceMuted self.mapView.compassView.isHidden = true - self.mapView.tracksUserCourse = true + self.mapView.tracksUserCourse = self.route != nil if let camera = pendingCamera { self.mapView.camera = camera From 2cbe904dae6e81d9a1e01f5a19b02dcd0f04f71a Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 21 May 2024 16:15:02 +0200 Subject: [PATCH 10/44] hide resume button with rest of UI --- MapboxNavigation/NavigationView.swift | 35 ++++++++++--------- .../NavigationViewController.swift | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index fcf1502da..150958cc0 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -177,31 +177,34 @@ open class NavigationView: UIView { } func showUI(animated: Bool = true) { + let views: [UIView] = [ + self.instructionsBannerContentView, + self.lanesView, + self.bottomBannerContentView, + self.floatingStackView + ] + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { - self.instructionsBannerContentView.alpha = 1 - self.lanesView.alpha = 1 - self.bottomBannerContentView.alpha = 1 - self.floatingStackView.alpha = 1 + views.forEach { $0.alpha = 1 } } completion: { _ in - self.instructionsBannerContentView.isHidden = false - self.lanesView.isHidden = false - self.bottomBannerContentView.isHidden = false - self.floatingStackView.isHidden = false + views.forEach { $0.isHidden = false } self.bottomBannerView.traitCollectionDidChange(self.traitCollection) } } func hideUI(animated: Bool = true) { + let views: [UIView] = [ + self.instructionsBannerContentView, + self.lanesView, + self.bottomBannerContentView, + self.floatingStackView, + self.resumeButton + ] + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { - self.instructionsBannerContentView.alpha = 0 - self.lanesView.alpha = 0 - self.bottomBannerContentView.alpha = 0 - self.floatingStackView.alpha = 0 + views.forEach { $0.alpha = 0 } } completion: { _ in - self.instructionsBannerContentView.isHidden = true - self.lanesView.isHidden = true - self.bottomBannerContentView.isHidden = true - self.floatingStackView.isHidden = true + views.forEach { $0.isHidden = true } self.bottomBannerView.traitCollectionDidChange(self.traitCollection) } } diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 5a20333d1..888acf1f1 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -444,9 +444,9 @@ open class NavigationViewController: UIViewController { public func start(with route: Route, locationManager: NavigationLocationManager? = nil) { self.locationManager = locationManager self.route = route - self.routeController?.resume() self.mapViewController?.navigationView.showUI(animated: true) + self.routeController?.resume() } public func endRoute(animated: Bool = true) { From f08b1bedfb98e30e289704ac9cf5a7d599677f63 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 21 May 2024 16:15:11 +0200 Subject: [PATCH 11/44] fix crash on end route --- Example/example/SceneDelegate.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index 1547bf716..a1127e251 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -25,10 +25,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window = UIWindow(windowScene: windowScene) let viewController = NavigationViewController() viewController.mapView?.styleURL = self.styleURL + viewController.showsEndOfRouteFeedback = false let waypoints = [ CLLocation(latitude: 52.032407, longitude: 5.580310), - CLLocation(latitude: 51.768686, longitude: 4.6827956) + CLLocation(latitude: 52.04, longitude: 5.580310) +// CLLocation(latitude: 51.768686, longitude: 4.6827956) ].map { Waypoint(location: $0) } viewController.mapView?.tracksUserCourse = false @@ -38,9 +40,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window?.rootViewController = viewController self.window?.makeKeyAndVisible() - -// return () - + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) options.shapeFormat = .polyline6 From 52b47f9d200d709e68b5404241b2c17b120fe08a Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 21 May 2024 18:13:49 +0200 Subject: [PATCH 12/44] add delegate callback when navigation arrived at destination --- Example/example/SceneDelegate.swift | 9 ++++++ .../NavigationViewController.swift | 17 ++++++++-- MapboxNavigation/RouteMapViewController.swift | 32 +++++++++++-------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index a1127e251..dc69fe331 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -37,6 +37,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { viewController.mapView?.showsUserLocation = true viewController.mapView?.zoomLevel = 12 viewController.mapView?.centerCoordinate = waypoints[0].coordinate + viewController.delegate = self self.window?.rootViewController = viewController self.window?.makeKeyAndVisible() @@ -86,3 +87,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } } + +extension SceneDelegate: NavigationViewControllerDelegate { + func navigationViewControllerDidArriveAtDestination(_ navigationViewController: NavigationViewController) { + navigationViewController.endRoute() + + print(#function) + } +} diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 888acf1f1..929288dbb 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -18,6 +18,14 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { - parameter canceled: True if the user dismissed the navigation view controller by tapping the Cancel button; false if the navigation view controller dismissed by some other means. */ @objc optional func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) + + /** + Called when user arrived at the destination of the trip. + + - parameter navigationViewController: The navigation view controller that finished navigation. + */ + @objc + optional func navigationViewControllerDidArriveAtDestination(_ navigationViewController: NavigationViewController) /** Called when the user arrives at the destination waypoint for a route leg. @@ -615,8 +623,13 @@ extension NavigationViewController: RouteControllerDelegate { let advancesToNextLeg = self.delegate?.navigationViewController?(self, didArriveAt: waypoint) ?? true if !self.isConnectedToCarPlay, // CarPlayManager shows rating on CarPlay if it's connected - routeController.routeProgress.isFinalLeg, advancesToNextLeg, self.showsEndOfRouteFeedback { - self.mapViewController?.showEndOfRoute { _ in } + routeController.routeProgress.isFinalLeg, advancesToNextLeg { + if self.showsEndOfRouteFeedback { + self.mapViewController?.showEndOfRoute { _ in } + } else { + self.mapViewController?.transitionToEndNavigation(with: 1) + self.delegate?.navigationViewControllerDidArriveAtDestination?(self) + } } return advancesToNextLeg } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index dc69de1b1..c0572e2c5 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -467,7 +467,24 @@ class RouteMapViewController: UIViewController { self.endOfRouteViewController.destination = self.destination self.navigationView.endOfRouteView?.isHidden = false - view.layoutIfNeeded() // flush layout queue + self.transitionToEndNavigation(with: duration, completion: completion) + + guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } + let insets = UIEdgeInsets(top: navigationView.instructionsBannerView.bounds.height, left: 20, bottom: height + 20, right: 20) + + if let coordinates = self.routeController?.routeProgress.route.coordinates, let userLocation = routeController?.locationManager.location?.coordinate { + let slicedLine = Polyline(coordinates).sliced(from: userLocation).coordinates + let line = MLNPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) + + let camera = self.navigationView.mapView.cameraThatFitsShape(line, direction: self.navigationView.mapView.camera.heading, edgePadding: insets) + camera.pitch = 0 + camera.altitude = self.navigationView.mapView.camera.altitude + self.navigationView.mapView.setCamera(camera, animated: true) + } + } + + func transitionToEndNavigation(with duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { + self.view.layoutIfNeeded() // flush layout queue NSLayoutConstraint.deactivate(self.navigationView.bannerShowConstraints) NSLayoutConstraint.activate(self.navigationView.bannerHideConstraints) self.navigationView.endOfRouteHideConstraint?.isActive = false @@ -487,19 +504,6 @@ class RouteMapViewController: UIViewController { self.navigationView.mapView.tracksUserCourse = false UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: completion) - - guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } - let insets = UIEdgeInsets(top: navigationView.instructionsBannerView.bounds.height, left: 20, bottom: height + 20, right: 20) - - if let coordinates = self.routeController?.routeProgress.route.coordinates, let userLocation = routeController?.locationManager.location?.coordinate { - let slicedLine = Polyline(coordinates).sliced(from: userLocation).coordinates - let line = MLNPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) - - let camera = self.navigationView.mapView.cameraThatFitsShape(line, direction: self.navigationView.mapView.camera.heading, edgePadding: insets) - camera.pitch = 0 - camera.altitude = self.navigationView.mapView.camera.altitude - self.navigationView.mapView.setCamera(camera, animated: true) - } } func hideEndOfRoute(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) { From 6d88391ec2aeb584b34196893a7adb9a6d2c581c Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 23 May 2024 12:41:52 +0200 Subject: [PATCH 13/44] remove deprecated code (below iOS11) --- MapboxNavigation/CarPlayManager.swift | 8 +-- .../CarPlayMapViewController.swift | 2 +- .../CarPlayNavigationViewController.swift | 2 +- .../EndOfRouteViewController.swift | 2 +- MapboxNavigation/NavigationMapView.swift | 2 +- MapboxNavigation/NavigationView.swift | 4 +- .../NavigationViewController.swift | 6 +- MapboxNavigation/NavigationViewLayout.swift | 62 +++++++++---------- MapboxNavigation/RouteMapViewController.swift | 6 +- MapboxNavigation/RouteVoiceController.swift | 2 +- MapboxNavigation/StatusView.swift | 8 +-- MapboxNavigation/StepsViewController.swift | 38 ++++++------ MapboxNavigation/UIView.swift | 47 -------------- 13 files changed, 67 insertions(+), 122 deletions(-) diff --git a/MapboxNavigation/CarPlayManager.swift b/MapboxNavigation/CarPlayManager.swift index e600264fa..4e315829f 100644 --- a/MapboxNavigation/CarPlayManager.swift +++ b/MapboxNavigation/CarPlayManager.swift @@ -574,7 +574,7 @@ extension CarPlayManager: CPMapTemplateDelegate { topDownCamera.pitch = 0 mapView.setCamera(topDownCamera, animated: false) - let padding = NavigationMapView.defaultPadding + mapView.safeArea + let padding = NavigationMapView.defaultPadding + mapView.safeAreaInsets mapView.showcase([route], padding: padding) } @@ -628,7 +628,7 @@ extension CarPlayManager: CPMapTemplateDelegate { return } - mapView.setContentInset(mapView.safeArea, animated: false, completionHandler: nil) // make sure this is always up to date in-case safe area changes during gesture + mapView.setContentInset(mapView.safeAreaInsets, animated: false, completionHandler: nil) // make sure this is always up to date in-case safe area changes during gesture self.updatePan(by: translation, mapTemplate: mapTemplate, animated: false) } @@ -647,7 +647,7 @@ extension CarPlayManager: CPMapTemplateDelegate { } func coordinate(of offset: CGPoint, in mapView: NavigationMapView) -> CLLocationCoordinate2D { - let contentFrame = mapView.bounds.inset(by: mapView.safeArea) + let contentFrame = mapView.bounds.inset(by: mapView.safeAreaInsets) let centerPoint = CGPoint(x: contentFrame.midX, y: contentFrame.midY) let endCameraPoint = CGPoint(x: centerPoint.x - offset.x, y: centerPoint.y - offset.y) @@ -661,7 +661,7 @@ extension CarPlayManager: CPMapTemplateDelegate { // Determine the screen distance to pan by based on the distance from the visual center to the closest side. let mapView = carPlayMapViewController.mapView - let contentFrame = mapView.bounds.inset(by: mapView.safeArea) + let contentFrame = mapView.bounds.inset(by: mapView.safeAreaInsets) let increment = min(mapView.bounds.width, mapView.bounds.height) / 2.0 // Calculate the distance in physical units from the visual center to where it would be after panning downwards. diff --git a/MapboxNavigation/CarPlayMapViewController.swift b/MapboxNavigation/CarPlayMapViewController.swift index 79e4a6a78..fa6040487 100644 --- a/MapboxNavigation/CarPlayMapViewController.swift +++ b/MapboxNavigation/CarPlayMapViewController.swift @@ -98,7 +98,7 @@ class CarPlayMapViewController: UIViewController, MLNMapViewDelegate { } override func viewSafeAreaInsetsDidChange() { - self.mapView.setContentInset(self.mapView.safeArea, animated: false, completionHandler: nil) + self.mapView.setContentInset(self.mapView.safeAreaInsets, animated: false, completionHandler: nil) guard self.isOverviewingRoutes else { super.viewSafeAreaInsetsDidChange() diff --git a/MapboxNavigation/CarPlayNavigationViewController.swift b/MapboxNavigation/CarPlayNavigationViewController.swift index 7789e3809..406f36e04 100644 --- a/MapboxNavigation/CarPlayNavigationViewController.swift +++ b/MapboxNavigation/CarPlayNavigationViewController.swift @@ -246,7 +246,7 @@ public class CarPlayNavigationViewController: UIViewController, MLNMapViewDelega // Estimating the width of Apple's maneuver view let bounds: () -> (CGRect) = { - let widthOfManeuverView = min(self.view.bounds.width - self.view.safeArea.left, self.view.bounds.width - self.view.safeArea.right) + let widthOfManeuverView = min(self.view.bounds.width - self.view.safeAreaInsets.left, self.view.bounds.width - self.view.safeAreaInsets.right) return CGRect(x: 0, y: 0, width: widthOfManeuverView, height: 30) } diff --git a/MapboxNavigation/EndOfRouteViewController.swift b/MapboxNavigation/EndOfRouteViewController.swift index 31b026dc0..92ba0a0a9 100644 --- a/MapboxNavigation/EndOfRouteViewController.swift +++ b/MapboxNavigation/EndOfRouteViewController.swift @@ -151,7 +151,7 @@ class EndOfRouteViewController: UIViewController { private func height(for height: ContainerHeight) -> CGFloat { let window = UIApplication.shared.keyWindow - let bottomMargin = window!.safeArea.bottom + let bottomMargin = window!.safeAreaInsets.bottom return height.rawValue + bottomMargin } diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index 8efbd9c3c..69636d1fe 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -132,7 +132,7 @@ open class NavigationMapView: MLNMapView, UIGestureRecognizerDelegate { return anchorPoint } - let contentFrame = bounds.inset(by: safeArea) + let contentFrame = bounds.inset(by: safeAreaInsets) let courseViewWidth = self.userCourseView?.frame.width ?? 0 let courseViewHeight = self.userCourseView?.frame.height ?? 0 let edgePadding = UIEdgeInsets(top: 50 + courseViewHeight / 2, diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index 150958cc0..0e2419047 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -50,7 +50,7 @@ open class NavigationView: UIView { } lazy var bannerShowConstraints: [NSLayoutConstraint] = [ - self.instructionsBannerView.topAnchor.constraint(equalTo: self.safeTopAnchor), + self.instructionsBannerView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor), self.instructionsBannerContentView.topAnchor.constraint(equalTo: self.topAnchor) ] @@ -59,7 +59,7 @@ open class NavigationView: UIView { self.instructionsBannerContentView.topAnchor.constraint(equalTo: self.instructionsBannerView.topAnchor) ] - lazy var endOfRouteShowConstraint: NSLayoutConstraint? = self.endOfRouteView?.bottomAnchor.constraint(equalTo: self.safeBottomAnchor) + lazy var endOfRouteShowConstraint: NSLayoutConstraint? = self.endOfRouteView?.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor) lazy var endOfRouteHideConstraint: NSLayoutConstraint? = self.endOfRouteView?.topAnchor.constraint(equalTo: self.bottomAnchor) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 929288dbb..4521ec81e 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -681,11 +681,7 @@ extension NavigationViewController: StyleManagerDelegate { private extension NavigationViewController { var isConnectedToCarPlay: Bool { - if #available(iOS 12.0, *) { - CarPlayManager.shared.isConnectedToCarPlay - } else { - false - } + CarPlayManager.shared.isConnectedToCarPlay } func resumeNotifications() { diff --git a/MapboxNavigation/NavigationViewLayout.swift b/MapboxNavigation/NavigationViewLayout.swift index c0ef5c844..8bd264560 100644 --- a/MapboxNavigation/NavigationViewLayout.swift +++ b/MapboxNavigation/NavigationViewLayout.swift @@ -3,50 +3,50 @@ import UIKit extension NavigationView { func setupConstraints() { NSLayoutConstraint.activate([ - mapView.topAnchor.constraint(equalTo: topAnchor), - mapView.leadingAnchor.constraint(equalTo: leadingAnchor), - mapView.bottomAnchor.constraint(equalTo: bottomAnchor), - mapView.trailingAnchor.constraint(equalTo: trailingAnchor), + self.mapView.topAnchor.constraint(equalTo: self.topAnchor), + self.mapView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.mapView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + self.mapView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - instructionsBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor), - instructionsBannerContentView.bottomAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor), - instructionsBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor), + self.instructionsBannerContentView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.instructionsBannerContentView.bottomAnchor.constraint(equalTo: self.instructionsBannerView.bottomAnchor), + self.instructionsBannerContentView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - instructionsBannerView.leadingAnchor.constraint(equalTo: leadingAnchor), - instructionsBannerView.trailingAnchor.constraint(equalTo: trailingAnchor), - instructionsBannerView.heightAnchor.constraint(equalToConstant: 96), + self.instructionsBannerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.instructionsBannerView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + self.instructionsBannerView.heightAnchor.constraint(equalToConstant: 96), - informationStackView.topAnchor.constraint(equalTo: instructionsBannerView.bottomAnchor), - informationStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - informationStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + self.informationStackView.topAnchor.constraint(equalTo: self.instructionsBannerView.bottomAnchor), + self.informationStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.informationStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - floatingStackView.topAnchor.constraint(equalTo: informationStackView.bottomAnchor, constant: 10), - floatingStackView.trailingAnchor.constraint(equalTo: safeTrailingAnchor, constant: -10), + self.floatingStackView.topAnchor.constraint(equalTo: self.informationStackView.bottomAnchor, constant: 10), + self.floatingStackView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: -10), - resumeButton.leadingAnchor.constraint(equalTo: safeLeadingAnchor, constant: 10), - resumeButton.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10), + self.resumeButton.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 10), + self.resumeButton.bottomAnchor.constraint(equalTo: self.bottomBannerView.topAnchor, constant: -10), - bottomBannerContentView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomBannerContentView.leadingAnchor.constraint(equalTo: leadingAnchor), - bottomBannerContentView.bottomAnchor.constraint(equalTo: bottomAnchor), - bottomBannerContentView.topAnchor.constraint(equalTo: bottomBannerView.topAnchor), + self.bottomBannerContentView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + self.bottomBannerContentView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.bottomBannerContentView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + self.bottomBannerContentView.topAnchor.constraint(equalTo: self.bottomBannerView.topAnchor), - bottomBannerView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomBannerView.leadingAnchor.constraint(equalTo: leadingAnchor), - bottomBannerView.bottomAnchor.constraint(equalTo: safeBottomAnchor), + self.bottomBannerView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + self.bottomBannerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.bottomBannerView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor), - wayNameView.centerXAnchor.constraint(equalTo: centerXAnchor), - wayNameView.bottomAnchor.constraint(equalTo: bottomBannerView.topAnchor, constant: -10) + self.wayNameView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + self.wayNameView.bottomAnchor.constraint(equalTo: self.bottomBannerView.topAnchor, constant: -10) ]) - NSLayoutConstraint.activate(bannerShowConstraints) + NSLayoutConstraint.activate(self.bannerShowConstraints) } func constrainEndOfRoute() { - endOfRouteHideConstraint?.isActive = true + self.endOfRouteHideConstraint?.isActive = true - endOfRouteView?.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - endOfRouteView?.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + self.endOfRouteView?.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + self.endOfRouteView?.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - endOfRouteHeightConstraint?.isActive = true + self.endOfRouteHeightConstraint?.isActive = true } } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index c0572e2c5..40f8ed32a 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -724,11 +724,7 @@ private extension RouteMapViewController { let options = (duration: duration, curve: curve) let keyboardHeight = keyBoardRect.size.height - if #available(iOS 11.0, *) { - navigationView.endOfRouteShowConstraint?.constant = -1 * (keyboardHeight - view.safeAreaInsets.bottom) // subtract the safe area, which is part of the keyboard's frame - } else { - self.navigationView.endOfRouteShowConstraint?.constant = -1 * keyboardHeight - } + self.navigationView.endOfRouteShowConstraint?.constant = -1 * (keyboardHeight - view.safeAreaInsets.bottom) // subtract the safe area, which is part of the keyboard's frame let opts = UIView.AnimationOptions(curve: options.curve) UIView.animate(withDuration: options.duration, delay: 0, options: opts, animations: view.layoutIfNeeded, completion: nil) diff --git a/MapboxNavigation/RouteVoiceController.swift b/MapboxNavigation/RouteVoiceController.swift index 121be56d8..3b4ce7588 100644 --- a/MapboxNavigation/RouteVoiceController.swift +++ b/MapboxNavigation/RouteVoiceController.swift @@ -212,7 +212,7 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate { utterance = AVSpeechUtterance(string: modifiedInstruction.text) utterance.voice = AVSpeechSynthesisVoice(identifier: AVSpeechSynthesisVoiceIdentifierAlex) } else { - if #available(iOS 10.0, *), !ignoreProgress, let routeProgress { + if !ignoreProgress, let routeProgress { utterance = AVSpeechUtterance(attributedString: modifiedInstruction.attributedText(for: routeProgress.currentLegProgress)) } else { utterance = AVSpeechUtterance(string: modifiedInstruction.text) diff --git a/MapboxNavigation/StatusView.swift b/MapboxNavigation/StatusView.swift index 2f01501dc..4eea28d71 100644 --- a/MapboxNavigation/StatusView.swift +++ b/MapboxNavigation/StatusView.swift @@ -59,11 +59,11 @@ public class StatusView: UIView { let heightConstraint = heightAnchor.constraint(equalToConstant: 30) heightConstraint.priority = UILayoutPriority(rawValue: 999) heightConstraint.isActive = true - textLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - textLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + textLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true + textLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true - activityIndicatorView.rightAnchor.constraint(equalTo: safeRightAnchor, constant: -10).isActive = true - activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + activityIndicatorView.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true + activityIndicatorView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true let recognizer = UIPanGestureRecognizer(target: self, action: #selector(StatusView.pan(_:))) addGestureRecognizer(recognizer) diff --git a/MapboxNavigation/StepsViewController.swift b/MapboxNavigation/StepsViewController.swift index 53977b65c..003f82940 100644 --- a/MapboxNavigation/StepsViewController.swift +++ b/MapboxNavigation/StepsViewController.swift @@ -112,26 +112,26 @@ public class StepsViewController: UIViewController { } func setupViews() { - view.translatesAutoresizingMaskIntoConstraints = false + self.view.translatesAutoresizingMaskIntoConstraints = false let backgroundView = StepsBackgroundView() backgroundView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(backgroundView) + self.view.addSubview(backgroundView) self.backgroundView = backgroundView - backgroundView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + backgroundView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true + backgroundView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true + backgroundView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true + backgroundView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true - let tableView = UITableView(frame: view.bounds, style: .plain) + let tableView = UITableView(frame: self.view.bounds, style: .plain) tableView.separatorColor = .clear tableView.backgroundColor = .clear tableView.backgroundView = nil tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self tableView.dataSource = self - view.addSubview(tableView) + self.view.addSubview(tableView) self.tableView = tableView let dismissButton = DismissButton(type: .custom) @@ -139,13 +139,13 @@ public class StepsViewController: UIViewController { let title = NSLocalizedString("DISMISS_STEPS_TITLE", bundle: .mapboxNavigation, value: "Close", comment: "Dismiss button title on the steps view") dismissButton.setTitle(title, for: .normal) dismissButton.addTarget(self, action: #selector(StepsViewController.tappedDismiss(_:)), for: .touchUpInside) - view.addSubview(dismissButton) + self.view.addSubview(dismissButton) self.dismissButton = dismissButton let bottomView = UIView() bottomView.translatesAutoresizingMaskIntoConstraints = false bottomView.backgroundColor = DismissButton.appearance().backgroundColor - view.addSubview(bottomView) + self.view.addSubview(bottomView) self.bottomView = bottomView let separatorBottomView = SeparatorView() @@ -154,24 +154,24 @@ public class StepsViewController: UIViewController { self.separatorBottomView = separatorBottomView dismissButton.heightAnchor.constraint(equalToConstant: 54).isActive = true - dismissButton.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - dismissButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true - dismissButton.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + dismissButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true + dismissButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true + dismissButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true bottomView.topAnchor.constraint(equalTo: dismissButton.bottomAnchor).isActive = true - bottomView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - bottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - bottomView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + bottomView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true + bottomView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true + bottomView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true separatorBottomView.topAnchor.constraint(equalTo: dismissButton.topAnchor).isActive = true separatorBottomView.leadingAnchor.constraint(equalTo: dismissButton.leadingAnchor).isActive = true separatorBottomView.trailingAnchor.constraint(equalTo: dismissButton.trailingAnchor).isActive = true separatorBottomView.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true - tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + tableView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true + tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true tableView.bottomAnchor.constraint(equalTo: dismissButton.topAnchor).isActive = true - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true tableView.register(StepTableViewCell.self, forCellReuseIdentifier: self.cellId) } diff --git a/MapboxNavigation/UIView.swift b/MapboxNavigation/UIView.swift index 920016154..91512381b 100644 --- a/MapboxNavigation/UIView.swift +++ b/MapboxNavigation/UIView.swift @@ -89,53 +89,6 @@ extension UIView { return view } - var safeArea: UIEdgeInsets { - guard #available(iOS 11.0, *) else { return .zero } - return safeAreaInsets - } - - var safeTopAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.topAnchor - } - return topAnchor - } - - var safeLeftAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.leftAnchor - } - return leftAnchor - } - - var safeLeadingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.leadingAnchor - } - return leadingAnchor - } - - var safeBottomAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.bottomAnchor - } - return bottomAnchor - } - - var safeRightAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.rightAnchor - } - return rightAnchor - } - - var safeTrailingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.trailingAnchor - } - return trailingAnchor - } - var imageRepresentation: UIImage? { let size = CGSize(width: frame.size.width, height: frame.size.height) UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale) From 4d3ef403786120a334e88a83e440873b78bd418f Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 23 May 2024 14:23:19 +0200 Subject: [PATCH 14/44] correctly show banner on second navigation --- MapboxNavigation/NavigationView.swift | 10 ++++++++++ MapboxNavigation/RouteMapViewController.swift | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index 0e2419047..11ebedc85 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -184,6 +184,11 @@ open class NavigationView: UIView { self.floatingStackView ] + NSLayoutConstraint.activate(self.bannerShowConstraints) + NSLayoutConstraint.deactivate(self.bannerHideConstraints) + self.endOfRouteHideConstraint?.isActive = true + self.endOfRouteShowConstraint?.isActive = false + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { views.forEach { $0.alpha = 1 } } completion: { _ in @@ -201,6 +206,11 @@ open class NavigationView: UIView { self.resumeButton ] + // NSLayoutConstraint.deactivate(self.navigationView.bannerShowConstraints) + // NSLayoutConstraint.activate(self.navigationView.bannerHideConstraints) + // self.navigationView.endOfRouteHideConstraint?.isActive = false + // self.navigationView.endOfRouteShowConstraint?.isActive = true + UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { views.forEach { $0.alpha = 0 } } completion: { _ in diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 40f8ed32a..6ae91eb18 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -507,10 +507,10 @@ class RouteMapViewController: UIViewController { } func hideEndOfRoute(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) { - view.layoutIfNeeded() // flush layout queue + self.view.layoutIfNeeded() // flush layout queue self.navigationView.endOfRouteHideConstraint?.isActive = true self.navigationView.endOfRouteShowConstraint?.isActive = false - view.clipsToBounds = true + self.view.clipsToBounds = true self.mapView.enableFrameByFrameCourseViewTracking(for: duration) self.mapView.setNeedsUpdateConstraints() From 53aedb13920dd135253a1796eb4326ba5d4c3157 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 23 May 2024 14:30:20 +0200 Subject: [PATCH 15/44] Remove Feedback form --- Example/example/SceneDelegate.swift | 1 - MapboxNavigation/NavigationView.swift | 49 ++------ .../NavigationViewController.swift | 28 +---- MapboxNavigation/NavigationViewLayout.swift | 9 -- MapboxNavigation/RouteMapViewController.swift | 111 +----------------- 5 files changed, 20 insertions(+), 178 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index dc69fe331..05f9a8f27 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -25,7 +25,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window = UIWindow(windowScene: windowScene) let viewController = NavigationViewController() viewController.mapView?.styleURL = self.styleURL - viewController.showsEndOfRouteFeedback = false let waypoints = [ CLLocation(latitude: 52.032407, longitude: 5.580310), diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index 11ebedc85..90d87f31d 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -59,12 +59,6 @@ open class NavigationView: UIView { self.instructionsBannerContentView.topAnchor.constraint(equalTo: self.instructionsBannerView.topAnchor) ] - lazy var endOfRouteShowConstraint: NSLayoutConstraint? = self.endOfRouteView?.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor) - - lazy var endOfRouteHideConstraint: NSLayoutConstraint? = self.endOfRouteView?.topAnchor.constraint(equalTo: self.bottomAnchor) - - lazy var endOfRouteHeightConstraint: NSLayoutConstraint? = self.endOfRouteView?.heightAnchor.constraint(equalToConstant: Constants.endOfRouteHeight) - private enum Images { static let overview = UIImage(named: "overview", in: .mapboxNavigation, compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) static let volumeUp = UIImage(named: "volume_up", in: .mapboxNavigation, compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) @@ -104,7 +98,6 @@ open class NavigationView: UIView { lazy var overviewButton = FloatingButton.rounded(image: Images.overview) lazy var muteButton = FloatingButton.rounded(image: Images.volumeUp, selectedImage: Images.volumeOff) - lazy var reportButton = FloatingButton.rounded(image: Images.feedback) lazy var lanesView: LanesView = .forAutoLayout(hidden: true) lazy var nextBannerView: NextBannerView = .forAutoLayout(hidden: true) @@ -137,18 +130,6 @@ open class NavigationView: UIView { } } - var endOfRouteView: UIView? { - didSet { - if let active: [NSLayoutConstraint] = constraints(affecting: oldValue) { - NSLayoutConstraint.deactivate(active) - } - - oldValue?.removeFromSuperview() - if let eor = endOfRouteView { addSubview(eor) } - self.endOfRouteView?.translatesAutoresizingMaskIntoConstraints = false - } - } - // MARK: - Lifecycle convenience init(delegate: NavigationViewDelegate) { @@ -186,8 +167,6 @@ open class NavigationView: UIView { NSLayoutConstraint.activate(self.bannerShowConstraints) NSLayoutConstraint.deactivate(self.bannerHideConstraints) - self.endOfRouteHideConstraint?.isActive = true - self.endOfRouteShowConstraint?.isActive = false UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { views.forEach { $0.alpha = 1 } @@ -206,10 +185,8 @@ open class NavigationView: UIView { self.resumeButton ] - // NSLayoutConstraint.deactivate(self.navigationView.bannerShowConstraints) - // NSLayoutConstraint.activate(self.navigationView.bannerHideConstraints) - // self.navigationView.endOfRouteHideConstraint?.isActive = false - // self.navigationView.endOfRouteShowConstraint?.isActive = true + NSLayoutConstraint.deactivate(self.bannerShowConstraints) + NSLayoutConstraint.activate(self.bannerHideConstraints) UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0) { views.forEach { $0.alpha = 0 } @@ -235,11 +212,11 @@ private extension NavigationView { func setupStackViews() { self.setupInformationStackView() - self.floatingStackView.addArrangedSubviews([self.overviewButton, self.muteButton, self.reportButton]) + self.floatingStackView.addArrangedSubviews([self.overviewButton, self.muteButton]) } func setupInformationStackView() { - let informationChildren: [UIView] = [instructionsBannerView, lanesView, nextBannerView, statusView] + let informationChildren: [UIView] = [self.instructionsBannerView, self.lanesView, self.nextBannerView, self.statusView] self.informationStackView.addArrangedSubviews(informationChildren) for informationChild in informationChildren { @@ -250,8 +227,8 @@ private extension NavigationView { func setupContainers() { let containers: [(UIView, UIView)] = [ - (instructionsBannerContentView, instructionsBannerView), - (bottomBannerContentView, bottomBannerView) + (self.instructionsBannerContentView, self.instructionsBannerView), + (self.bottomBannerContentView, self.bottomBannerView) ] containers.forEach { $0.addSubview($1) } } @@ -261,13 +238,13 @@ private extension NavigationView { self.setupContainers() let subviews: [UIView] = [ - mapView, - informationStackView, - floatingStackView, - resumeButton, - wayNameView, - bottomBannerContentView, - instructionsBannerContentView + self.mapView, + self.informationStackView, + self.floatingStackView, + self.resumeButton, + self.wayNameView, + self.bottomBannerContentView, + self.instructionsBannerContentView ] subviews.forEach(addSubview(_:)) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 4521ec81e..208f7cd7d 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -297,25 +297,6 @@ open class NavigationViewController: UIViewController { */ public var sendsNotifications: Bool = true - /** - Shows a button that allows drivers to report feedback such as accidents, closed roads, poor instructions, etc. Defaults to `true`. - */ - public var showsReportFeedback: Bool = true { - didSet { - self.mapViewController?.reportButton.isHidden = !self.showsReportFeedback - self.showsEndOfRouteFeedback = self.showsReportFeedback - } - } - - /** - Shows End of route Feedback UI when the route controller arrives at the final destination. Defaults to `true.` - */ - public var showsEndOfRouteFeedback: Bool = true { - didSet { - self.mapViewController?.showsEndOfRoute = self.showsEndOfRouteFeedback - } - } - /** If true, the map style and UI will automatically be updated given the time of day. */ @@ -395,7 +376,6 @@ open class NavigationViewController: UIViewController { } mapSubview.pinInSuperview() - mapViewController.reportButton.isHidden = !self.showsReportFeedback self.styleManager = StyleManager(self) self.styleManager.styles = styles ?? [DayStyle(), NightStyle()] @@ -624,12 +604,8 @@ extension NavigationViewController: RouteControllerDelegate { if !self.isConnectedToCarPlay, // CarPlayManager shows rating on CarPlay if it's connected routeController.routeProgress.isFinalLeg, advancesToNextLeg { - if self.showsEndOfRouteFeedback { - self.mapViewController?.showEndOfRoute { _ in } - } else { - self.mapViewController?.transitionToEndNavigation(with: 1) - self.delegate?.navigationViewControllerDidArriveAtDestination?(self) - } + self.mapViewController?.transitionToEndNavigation(with: 1) + self.delegate?.navigationViewControllerDidArriveAtDestination?(self) } return advancesToNextLeg } diff --git a/MapboxNavigation/NavigationViewLayout.swift b/MapboxNavigation/NavigationViewLayout.swift index 8bd264560..456c4cad4 100644 --- a/MapboxNavigation/NavigationViewLayout.swift +++ b/MapboxNavigation/NavigationViewLayout.swift @@ -40,13 +40,4 @@ extension NavigationView { ]) NSLayoutConstraint.activate(self.bannerShowConstraints) } - - func constrainEndOfRoute() { - self.endOfRouteHideConstraint?.isActive = true - - self.endOfRouteView?.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - self.endOfRouteView?.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - - self.endOfRouteHeightConstraint?.isActive = true - } } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 6ae91eb18..dcaf74654 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -41,7 +41,6 @@ class RouteMapViewController: UIViewController { var navigationView: NavigationView { view as! NavigationView } var mapView: NavigationMapView { self.navigationView.mapView } var statusView: StatusView { self.navigationView.statusView } - var reportButton: FloatingButton { self.navigationView.reportButton } var lanesView: LanesView { self.navigationView.lanesView } var nextBannerView: NextBannerView { self.navigationView.nextBannerView } var instructionsBannerView: InstructionsBannerView { self.navigationView.instructionsBannerView } @@ -222,9 +221,7 @@ class RouteMapViewController: UIViewController { // MARK: - UIContentContainer override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - self.navigationView.endOfRouteHeightConstraint?.constant = container.preferredContentSize.height - - UIView.animate(withDuration: 0.3, animations: view.layoutIfNeeded) + UIView.animate(withDuration: 0.3, animations: self.view.layoutIfNeeded) } // MARK: - RouteMapViewController @@ -442,53 +439,11 @@ class RouteMapViewController: UIViewController { } // MARK: End Of Route - - func embedEndOfRoute() { - let endOfRoute = self.endOfRouteViewController - addChild(endOfRoute) - self.navigationView.endOfRouteView = endOfRoute.view - self.navigationView.constrainEndOfRoute() - endOfRoute.didMove(toParent: self) - - endOfRoute.dismissHandler = { [weak self] _, _ in - self?.routeController?.endNavigation() - self?.delegate?.mapViewControllerDidDismiss(self!, byCanceling: false) - } - } - - func unembedEndOfRoute() { - let endOfRoute = self.endOfRouteViewController - endOfRoute.willMove(toParent: nil) - endOfRoute.removeFromParent() - } - - func showEndOfRoute(duration: TimeInterval = 1.0, completion: ((Bool) -> Void)? = nil) { - self.embedEndOfRoute() - self.endOfRouteViewController.destination = self.destination - self.navigationView.endOfRouteView?.isHidden = false - - self.transitionToEndNavigation(with: duration, completion: completion) - - guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } - let insets = UIEdgeInsets(top: navigationView.instructionsBannerView.bounds.height, left: 20, bottom: height + 20, right: 20) - - if let coordinates = self.routeController?.routeProgress.route.coordinates, let userLocation = routeController?.locationManager.location?.coordinate { - let slicedLine = Polyline(coordinates).sliced(from: userLocation).coordinates - let line = MLNPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) - - let camera = self.navigationView.mapView.cameraThatFitsShape(line, direction: self.navigationView.mapView.camera.heading, edgePadding: insets) - camera.pitch = 0 - camera.altitude = self.navigationView.mapView.camera.altitude - self.navigationView.mapView.setCamera(camera, animated: true) - } - } func transitionToEndNavigation(with duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { self.view.layoutIfNeeded() // flush layout queue NSLayoutConstraint.deactivate(self.navigationView.bannerShowConstraints) NSLayoutConstraint.activate(self.navigationView.bannerHideConstraints) - self.navigationView.endOfRouteHideConstraint?.isActive = false - self.navigationView.endOfRouteShowConstraint?.isActive = true self.mapView.enableFrameByFrameCourseViewTracking(for: duration) self.mapView.setNeedsUpdateConstraints() @@ -508,8 +463,6 @@ class RouteMapViewController: UIViewController { func hideEndOfRoute(duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) { self.view.layoutIfNeeded() // flush layout queue - self.navigationView.endOfRouteHideConstraint?.isActive = true - self.navigationView.endOfRouteShowConstraint?.isActive = false self.view.clipsToBounds = true self.mapView.enableFrameByFrameCourseViewTracking(for: duration) @@ -520,19 +473,14 @@ class RouteMapViewController: UIViewController { self.navigationView.floatingStackView.alpha = 1.0 } - let complete: (Bool) -> Void = { - self.navigationView.endOfRouteView?.isHidden = true - self.unembedEndOfRoute() - completion?($0) - } - let noAnimation = { animate() - complete(true) + completion?(true) } guard duration > 0.0 else { return noAnimation() } - UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: complete) + + UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: completion) } } @@ -641,13 +589,7 @@ extension RouteMapViewController: NavigationViewDelegate { } func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { - // If the end of route component is showing, then put the anchor point slightly above the middle of the map - if self.navigationView.endOfRouteView != nil, let show = navigationView.endOfRouteShowConstraint, show.isActive { - return CGPoint(x: mapView.bounds.midX, y: mapView.bounds.height * 0.4) - } - - // otherwise, ask the delegate or return .zero - return self.delegate?.navigationMapViewUserAnchorPoint?(mapView) ?? .zero + self.delegate?.navigationMapViewUserAnchorPoint?(mapView) ?? .zero } } @@ -712,37 +654,6 @@ extension RouteMapViewController: StepsViewControllerDelegate { // MARK: - Keyboard Handling private extension RouteMapViewController { - @objc - func keyboardWillShow(notification: NSNotification) { - guard self.navigationView.endOfRouteView != nil else { return } - guard let userInfo = notification.userInfo else { return } - guard let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int else { return } - guard let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return } - guard let keyBoardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } - - let curve = UIView.AnimationCurve(rawValue: curveValue) ?? UIView.AnimationCurve.easeIn - let options = (duration: duration, curve: curve) - let keyboardHeight = keyBoardRect.size.height - - self.navigationView.endOfRouteShowConstraint?.constant = -1 * (keyboardHeight - view.safeAreaInsets.bottom) // subtract the safe area, which is part of the keyboard's frame - - let opts = UIView.AnimationOptions(curve: options.curve) - UIView.animate(withDuration: options.duration, delay: 0, options: opts, animations: view.layoutIfNeeded, completion: nil) - } - - @objc - func keyboardWillHide(notification: NSNotification) { - guard self.navigationView.endOfRouteView != nil else { return } - guard let userInfo = notification.userInfo else { return } - let curve = UIView.AnimationCurve(rawValue: userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int) - let options = (duration: userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double, - curve: UIView.AnimationOptions(curve: curve!)) - - self.navigationView.endOfRouteShowConstraint?.constant = 0 - - UIView.animate(withDuration: options.duration, delay: 0, options: options.curve, animations: view.layoutIfNeeded, completion: nil) - } - @objc func recenter(_ sender: AnyObject) { self.mapView.tracksUserCourse = true @@ -833,16 +744,6 @@ private extension RouteMapViewController { self.navigationView.bottomBannerView.updateETA(routeProgress: routeController.routeProgress) } - func subscribeToKeyboardNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(RouteMapViewController.keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - func unsubscribeFromKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - func resumeNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(self.willReroute(notification:)), name: .routeControllerWillReroute, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.didReroute(notification:)), name: .routeControllerDidReroute, object: nil) @@ -850,7 +751,6 @@ private extension RouteMapViewController { NotificationCenter.default.addObserver(self, selector: #selector(self.applicationWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.removeTimer), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.updateInstructionsBanner(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: self.routeController) - self.subscribeToKeyboardNotifications() } func suspendNotifications() { @@ -860,7 +760,6 @@ private extension RouteMapViewController { NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassVisualInstructionPoint, object: nil) - self.unsubscribeFromKeyboardNotifications() } func showStatus(title: String, withSpinner spin: Bool = false, for time: TimeInterval, animated: Bool = true, interactive: Bool = false) { From 08e39b2828d93a546ef78531bf3b1ceb518fb54b Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 23 May 2024 14:39:52 +0200 Subject: [PATCH 16/44] remove route from NavigationViewController init --- .../NavigationViewController.swift | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 208f7cd7d..945dff7c4 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -337,32 +337,17 @@ open class NavigationViewController: UIViewController { See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. */ - @objc(initWithRoute:directions:styles:routeController:locationManager:voiceController:) - public required init(for route: Route? = nil, - directions: Directions = Directions.shared, - styles: [Style]? = [DayStyle(), NightStyle()], - routeController: RouteController? = nil, - locationManager: NavigationLocationManager? = nil, - voiceController: RouteVoiceController? = nil) { - self.route = route + @objc(initWithDirections:styles:voiceController:) + public required init(directions: Directions = Directions.shared, + styles: [Style] = [DayStyle(), NightStyle()], + voiceController: RouteVoiceController = RouteVoiceController()) { self.directions = directions - self.locationManager = locationManager ?? NavigationLocationManager() - if let route { - self.routeController = routeController ?? RouteController(along: route, directions: directions, locationManager: self.locationManager) - NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile - } - self.voiceController = voiceController ?? RouteVoiceController() + self.voiceController = voiceController super.init(nibName: nil, bundle: nil) - self.routeController?.usesDefaultUserInterface = true - self.routeController?.delegate = self - self.routeController?.tunnelIntersectionManager.delegate = self - self.routeController?.resume() - let mapViewController = RouteMapViewController(routeController: self.routeController, delegate: self) self.mapViewController = mapViewController - mapViewController.destination = route?.legs.last?.destination mapViewController.willMove(toParent: self) self.addChild(mapViewController) mapViewController.didMove(toParent: self) @@ -370,19 +355,13 @@ open class NavigationViewController: UIViewController { mapSubview.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(mapSubview) - if route == nil { - self.mapViewController?.navigationView.hideUI(animated: false) - self.mapView?.tracksUserCourse = false - } + self.mapViewController?.navigationView.hideUI(animated: false) + self.mapView?.tracksUserCourse = false mapSubview.pinInSuperview() self.styleManager = StyleManager(self) - self.styleManager.styles = styles ?? [DayStyle(), NightStyle()] - - if !(route?.routeOptions is NavigationRouteOptions) { - print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") - } + self.styleManager.styles = styles } deinit { @@ -429,12 +408,21 @@ open class NavigationViewController: UIViewController { // MARK: - NavigationViewController - public func start(with route: Route, locationManager: NavigationLocationManager? = nil) { + public func start(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) { self.locationManager = locationManager self.route = route self.mapViewController?.navigationView.showUI(animated: true) + self.mapViewController?.destination = route.legs.last?.destination + + self.routeController?.usesDefaultUserInterface = true + self.routeController?.delegate = self + self.routeController?.tunnelIntersectionManager.delegate = self self.routeController?.resume() + + if !(route.routeOptions is NavigationRouteOptions) { + print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") + } } public func endRoute(animated: Bool = true) { @@ -470,7 +458,8 @@ open class NavigationViewController: UIViewController { let locationManager = routeController.locationManager.copy() as! NavigationLocationManager let directions = routeController.directions let route = routeController.routeProgress.route - let navigationViewController = NavigationViewController(for: route, directions: directions, routeController: routeController, locationManager: locationManager) + let navigationViewController = NavigationViewController(directions: directions) + navigationViewController.start(with: route, routeController: routeController, locationManager: locationManager) window.rootViewController?.topMostViewController()?.present(navigationViewController, animated: true, completion: { navigationViewController.isUsedInConjunctionWithCarPlayWindow = true From bb87d680b6e0f6dff879544a05d84846af007f87 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 23 May 2024 14:49:27 +0200 Subject: [PATCH 17/44] add styleURL to NavigationViewController init --- Example/example/SceneDelegate.swift | 3 +- Example/example/ViewController.swift | 41 ------------------- .../NavigationViewController.swift | 13 +++--- 3 files changed, 8 insertions(+), 49 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index 05f9a8f27..cb3c758b2 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -23,8 +23,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } self.window = UIWindow(windowScene: windowScene) - let viewController = NavigationViewController() - viewController.mapView?.styleURL = self.styleURL + let viewController = NavigationViewController(styleURL: self.styleURL) let waypoints = [ CLLocation(latitude: 52.032407, longitude: 5.580310), diff --git a/Example/example/ViewController.swift b/Example/example/ViewController.swift index 949fda05e..189e19d52 100644 --- a/Example/example/ViewController.swift +++ b/Example/example/ViewController.swift @@ -18,47 +18,6 @@ class ViewController: NavigationViewController { self.mapView?.styleURL = self.styleURL self.mapView?.reloadStyle(nil) - -// let navigationView = NavigationMapView(frame: .zero, styleURL: self.styleURL, config: MNConfig()) -// self.navigationView = navigationView -// self.view.addSubview(navigationView) -// -// navigationView.translatesAutoresizingMaskIntoConstraints = false -// navigationView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true -// navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true -// navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true -// navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true -// -// let waypoints = [ -// CLLocation(latitude: 52.032407, longitude: 5.580310), -// CLLocation(latitude: 51.768686, longitude: 4.6827956) -// ].map { Waypoint(location: $0) } -// -// navigationView.zoomLevel = 12 -// navigationView.centerCoordinate = waypoints[0].coordinate -// -// let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) -// options.shapeFormat = .polyline6 -// options.distanceMeasurementSystem = .metric -// options.attributeOptions = [] -// -// print("[\(type(of: self))] Calculating routes with URL: \(Directions.shared.url(forCalculating: options))") -// -// let viewController = NavigationViewController() -// -// Directions.shared.calculate(options) { _, routes, _ in -// guard let route = routes?.first else { return } -// -// let simulatedLocationManager = SimulatedLocationManager(route: route) -// simulatedLocationManager.speedMultiplier = 2 -// -// viewController.mapView?.styleURL = self.styleURL -// self.present(viewController, animated: true) { -// DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { -// viewController.begin(with: route, locationManager: simulatedLocationManager) -// } -// } -// } } override func viewDidAppear(_ animated: Bool) { diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 945dff7c4..1423954cd 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -337,8 +337,9 @@ open class NavigationViewController: UIViewController { See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. */ - @objc(initWithDirections:styles:voiceController:) - public required init(directions: Directions = Directions.shared, + @objc(initWithStyleURL:directions:styles:voiceController:) + public required init(styleURL: URL? = nil, + directions: Directions = Directions.shared, styles: [Style] = [DayStyle(), NightStyle()], voiceController: RouteVoiceController = RouteVoiceController()) { self.directions = directions @@ -354,14 +355,14 @@ open class NavigationViewController: UIViewController { let mapSubview: UIView = mapViewController.view mapSubview.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(mapSubview) - - self.mapViewController?.navigationView.hideUI(animated: false) - self.mapView?.tracksUserCourse = false - mapSubview.pinInSuperview() self.styleManager = StyleManager(self) self.styleManager.styles = styles + + self.mapViewController?.navigationView.hideUI(animated: false) + self.mapView?.tracksUserCourse = false + self.mapView?.styleURL = styleURL } deinit { From 28d894c7e8033a69748811af284c337ea399063a Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 23 May 2024 15:29:15 +0200 Subject: [PATCH 18/44] add button to example to randomly position camera --- Example/example/SceneDelegate.swift | 125 ++++++++++-------- .../NavigationViewController.swift | 13 +- MapboxNavigation/RouteMapViewController.swift | 6 +- 3 files changed, 78 insertions(+), 66 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index cb3c758b2..b9ef58d2b 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -15,6 +15,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private let styleURL = Bundle.main.url(forResource: "Terrain", withExtension: "json")! // swiftlint:disable:this force_unwrapping var window: UIWindow? + var viewController: NavigationViewController! + var route: Route! + + let waypoints = [ + CLLocation(latitude: 52.032407, longitude: 5.580310), + CLLocation(latitude: 52.04, longitude: 5.580310), + CLLocation(latitude: 51.768686, longitude: 4.6827956) + ].map { Waypoint(location: $0) } func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. @@ -23,73 +31,80 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } self.window = UIWindow(windowScene: windowScene) - let viewController = NavigationViewController(styleURL: self.styleURL) - - let waypoints = [ - CLLocation(latitude: 52.032407, longitude: 5.580310), - CLLocation(latitude: 52.04, longitude: 5.580310) -// CLLocation(latitude: 51.768686, longitude: 4.6827956) - ].map { Waypoint(location: $0) } - - viewController.mapView?.tracksUserCourse = false - viewController.mapView?.showsUserLocation = true - viewController.mapView?.zoomLevel = 12 - viewController.mapView?.centerCoordinate = waypoints[0].coordinate - viewController.delegate = self + self.viewController = NavigationViewController(styleURL: self.styleURL) + self.viewController.mapView?.tracksUserCourse = false + self.viewController.mapView?.showsUserLocation = true + self.viewController.mapView?.zoomLevel = 12 + self.viewController.mapView?.centerCoordinate = self.waypoints[0].coordinate + self.viewController.delegate = self - self.window?.rootViewController = viewController + self.window?.rootViewController = self.viewController self.window?.makeKeyAndVisible() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { - let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) - options.shapeFormat = .polyline6 - options.distanceMeasurementSystem = .metric - options.attributeOptions = [] - - Directions.shared.calculate(options) { _, routes, _ in - guard let route = routes?.first else { return } - - let simulatedLocationManager = SimulatedLocationManager(route: route) - simulatedLocationManager.speedMultiplier = 2 - - viewController.start(with: route, locationManager: simulatedLocationManager) - } + self.startNavigation(for: Array(self.waypoints[0 ... 1])) } + + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(UIImage(systemName: "globe"), for: .normal) + button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + button.backgroundColor = .white + button.layer.cornerRadius = 8 + self.viewController.view.addSubview(button) + NSLayoutConstraint.activate([ + button.trailingAnchor.constraint(equalTo: self.viewController.view.layoutMarginsGuide.trailingAnchor), + button.centerYAnchor.constraint(equalTo: self.viewController.view.centerYAnchor), + button.widthAnchor.constraint(equalTo: button.heightAnchor), + button.widthAnchor.constraint(equalToConstant: 44) + ]) } +} - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). +extension SceneDelegate: NavigationViewControllerDelegate { + func navigationViewControllerDidFinish(_ navigationViewController: NavigationViewController) { + navigationViewController.endRoute() + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { + navigationViewController.start(with: self.route, locationManager: SimulatedLocationManager(route: self.route)) + } } +} - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } +// MARK: - Private - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. +private extension SceneDelegate { + func startNavigation(for waypoints: [Waypoint]) { + let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) + options.shapeFormat = .polyline6 + options.distanceMeasurementSystem = .metric + options.attributeOptions = [] + + Directions.shared.calculate(options) { _, routes, _ in + guard let route = routes?.first else { return } + + self.route = route + + let simulatedLocationManager = SimulatedLocationManager(route: route) + simulatedLocationManager.speedMultiplier = 2 + + self.viewController.start(with: route, locationManager: simulatedLocationManager) + } } -} + + @objc + func buttonTapped() { + guard let waypoint = self.waypoints.randomElement() else { return } + + func randomCLLocationDistance(min: CLLocationDistance, max: CLLocationDistance) -> CLLocationDistance { + CLLocationDistance(arc4random_uniform(UInt32(max - min)) + UInt32(min)) + } -extension SceneDelegate: NavigationViewControllerDelegate { - func navigationViewControllerDidArriveAtDestination(_ navigationViewController: NavigationViewController) { - navigationViewController.endRoute() + let distance = randomCLLocationDistance(min: 10, max: 100_000) - print(#function) + self.viewController.mapView?.camera = .init(lookingAtCenter: waypoint.coordinate, + acrossDistance: distance, + pitch: 0, + heading: 0) } } diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 1423954cd..eb9d882bd 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -25,7 +25,7 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { - parameter navigationViewController: The navigation view controller that finished navigation. */ @objc - optional func navigationViewControllerDidArriveAtDestination(_ navigationViewController: NavigationViewController) + optional func navigationViewControllerDidFinish(_ navigationViewController: NavigationViewController) /** Called when the user arrives at the destination waypoint for a route leg. @@ -427,18 +427,12 @@ open class NavigationViewController: UIViewController { } public func endRoute(animated: Bool = true) { - let route = self.route! - self.routeController?.endNavigation() self.mapView?.removeRoutes() self.voiceController = nil self.route = nil self.mapViewController?.navigationView.hideUI(animated: animated) - - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { - self.start(with: route, locationManager: SimulatedLocationManager(route: route)) - } } #if canImport(CarPlay) @@ -524,8 +518,9 @@ extension NavigationViewController: RouteMapViewControllerDelegate { self.delegate?.navigationViewController?(self, viewFor: annotation) } - func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { + func mapViewControllerDidFinish(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { self.endRoute() + self.delegate?.navigationViewControllerDidFinish?(self) } public func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { @@ -595,7 +590,7 @@ extension NavigationViewController: RouteControllerDelegate { if !self.isConnectedToCarPlay, // CarPlayManager shows rating on CarPlay if it's connected routeController.routeProgress.isFinalLeg, advancesToNextLeg { self.mapViewController?.transitionToEndNavigation(with: 1) - self.delegate?.navigationViewControllerDidArriveAtDestination?(self) + self.delegate?.navigationViewControllerDidFinish?(self) } return advancesToNextLeg } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index dcaf74654..b758ee60f 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -9,7 +9,7 @@ class ArrowFillPolyline: MLNPolylineFeature {} class ArrowStrokePolyline: ArrowFillPolyline {} @objc protocol RouteMapViewControllerDelegate: NavigationMapViewDelegate, MLNMapViewDelegate, VisualInstructionDelegate { - func mapViewControllerDidDismiss(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) + func mapViewControllerDidFinish(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) func mapViewControllerShouldAnnotateSpokenInstructions(_ routeMapViewController: RouteMapViewController) -> Bool /** @@ -490,7 +490,7 @@ extension RouteMapViewController: NavigationViewDelegate { // MARK: NavigationViewDelegate func navigationView(_ view: NavigationView, didTapCancelButton: CancelButton) { - self.delegate?.mapViewControllerDidDismiss(self, byCanceling: true) + self.delegate?.mapViewControllerDidFinish(self, byCanceling: true) } // MARK: MLNMapViewDelegate @@ -527,11 +527,13 @@ extension RouteMapViewController: NavigationViewDelegate { func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) { self.navigationView.resumeButton.isHidden = true + self.navigationView.resumeButton.alpha = 0 mapView.logoView.isHidden = false } func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) { self.navigationView.resumeButton.isHidden = false + self.navigationView.resumeButton.alpha = 1 self.navigationView.wayNameView.isHidden = true mapView.logoView.isHidden = true } From abf84ec4ca0afc6306ef0c4d4b0ac9cd88c8f34a Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 29 May 2024 18:32:18 +0200 Subject: [PATCH 19/44] restore map after ending route --- MapboxNavigation/NavigationViewController.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index eb9d882bd..a844c91ad 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -429,10 +429,21 @@ open class NavigationViewController: UIViewController { public func endRoute(animated: Bool = true) { self.routeController?.endNavigation() self.mapView?.removeRoutes() + self.mapView?.removeWaypoints() + self.mapView?.removeArrow() + self.voiceController = nil self.route = nil self.mapViewController?.navigationView.hideUI(animated: animated) + self.mapView?.tracksUserCourse = false + self.mapView?.userLocationForCourseTracking = nil + self.mapView?.showsUserLocation = true + + if let camera = self.mapView?.camera { + camera.pitch = 0 + self.mapView?.setCamera(camera, animated: false) + } } #if canImport(CarPlay) From 89dec63943e994537e0b8574ac29d2218bd62226 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 31 May 2024 20:41:43 +0200 Subject: [PATCH 20/44] whitespace changes --- MapboxNavigation/NavigationView.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MapboxNavigation/NavigationView.swift b/MapboxNavigation/NavigationView.swift index 90d87f31d..dcb64034b 100644 --- a/MapboxNavigation/NavigationView.swift +++ b/MapboxNavigation/NavigationView.swift @@ -209,22 +209,22 @@ private extension NavigationView { self.setupViews() self.setupConstraints() } - + func setupStackViews() { self.setupInformationStackView() self.floatingStackView.addArrangedSubviews([self.overviewButton, self.muteButton]) } - + func setupInformationStackView() { let informationChildren: [UIView] = [self.instructionsBannerView, self.lanesView, self.nextBannerView, self.statusView] self.informationStackView.addArrangedSubviews(informationChildren) - + for informationChild in informationChildren { informationChild.leadingAnchor.constraint(equalTo: self.informationStackView.leadingAnchor).isActive = true informationChild.trailingAnchor.constraint(equalTo: self.informationStackView.trailingAnchor).isActive = true } } - + func setupContainers() { let containers: [(UIView, UIView)] = [ (self.instructionsBannerContentView, self.instructionsBannerView), @@ -232,7 +232,7 @@ private extension NavigationView { ] containers.forEach { $0.addSubview($1) } } - + func setupViews() { self.setupStackViews() self.setupContainers() @@ -246,10 +246,10 @@ private extension NavigationView { self.bottomBannerContentView, self.instructionsBannerContentView ] - + subviews.forEach(addSubview(_:)) } - + func updateDelegates() { self.mapView.delegate = self.delegate self.mapView.navigationMapDelegate = self.delegate From 72b55be3fef831f1349bbe0c43dfb619ff8e1983 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 31 May 2024 21:15:10 +0200 Subject: [PATCH 21/44] unify start and stop navigation method names --- Example/example/SceneDelegate.swift | 6 +++--- .../NavigationViewController.swift | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index bb3bd49f8..b61b3afed 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -63,10 +63,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate: NavigationViewControllerDelegate { func navigationViewControllerDidFinish(_ navigationViewController: NavigationViewController) { - navigationViewController.endRoute() + navigationViewController.endNavigation() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { - navigationViewController.start(with: self.route, locationManager: SimulatedLocationManager(route: self.route)) + navigationViewController.startNavigation(with: self.route, locationManager: SimulatedLocationManager(route: self.route)) } } } @@ -88,7 +88,7 @@ private extension SceneDelegate { let simulatedLocationManager = SimulatedLocationManager(route: route) simulatedLocationManager.speedMultiplier = 2 - self.viewController.start(with: route, locationManager: simulatedLocationManager) + self.viewController.startNavigation(with: route, locationManager: simulatedLocationManager) } } diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 967e74bcd..76958a15a 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -409,12 +409,12 @@ open class NavigationViewController: UIViewController { }() assert(dayStyle.styleType == .day) assert(nightStyle.styleType == .night) - + self.directions = directions self.voiceController = voiceController - + super.init(nibName: nil, bundle: nil) - + let mapViewController = RouteMapViewController(routeController: self.routeController, delegate: self) self.mapViewController = mapViewController mapViewController.willMove(toParent: self) @@ -424,10 +424,10 @@ open class NavigationViewController: UIViewController { mapSubview.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(mapSubview) mapSubview.pinInSuperview() - + self.styleManager = StyleManager(self) self.styleManager.styles = [dayStyle, nightStyle] - + self.mapViewController?.navigationView.hideUI(animated: false) self.mapView?.tracksUserCourse = false } @@ -476,7 +476,7 @@ open class NavigationViewController: UIViewController { // MARK: - NavigationViewController - public func start(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) { + public func startNavigation(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) { self.locationManager = locationManager self.route = route @@ -493,7 +493,7 @@ open class NavigationViewController: UIViewController { } } - public func endRoute(animated: Bool = true) { + public func endNavigation(animated: Bool = true) { self.routeController?.endNavigation() self.mapView?.removeRoutes() self.mapView?.removeWaypoints() @@ -532,7 +532,7 @@ open class NavigationViewController: UIViewController { let directions = routeController.directions let route = routeController.routeProgress.route let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ()), directions: directions) - navigationViewController.start(with: route, routeController: routeController, locationManager: locationManager) + navigationViewController.startNavigation(with: route, routeController: routeController, locationManager: locationManager) window.rootViewController?.topMostViewController()?.present(navigationViewController, animated: true, completion: { navigationViewController.isUsedInConjunctionWithCarPlayWindow = true @@ -597,7 +597,7 @@ extension NavigationViewController: RouteMapViewControllerDelegate { } func mapViewControllerDidFinish(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { - self.endRoute() + self.endNavigation() self.delegate?.navigationViewControllerDidFinish?(self) } From cf1c944e23a79e3b9a07bd5c29da3e8c68fb4d0b Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 31 May 2024 21:21:40 +0200 Subject: [PATCH 22/44] remove external files --- Example/Example.xcodeproj/project.pbxproj | 12 - Example/example/SceneDelegate.swift | 2 - Example/example/Terrain.json | 1 - Example/example/Toursprung.swift | 302 ---------------------- Example/example/ViewController.swift | 29 --- 5 files changed, 346 deletions(-) delete mode 100644 Example/example/Terrain.json delete mode 100644 Example/example/Toursprung.swift delete mode 100644 Example/example/ViewController.swift diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 6f505f9c2..c4727dd1f 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -9,11 +9,8 @@ /* Begin PBXBuildFile section */ CD958B4B2BF6156100501F93 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4A2BF6156100501F93 /* AppDelegate.swift */; }; CD958B4D2BF6156100501F93 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4C2BF6156100501F93 /* SceneDelegate.swift */; }; - CD958B4F2BF6156100501F93 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B4E2BF6156100501F93 /* ViewController.swift */; }; CD958B542BF6156200501F93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD958B532BF6156200501F93 /* Assets.xcassets */; }; CD958B572BF6156200501F93 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CD958B562BF6156200501F93 /* Base */; }; - CD958B622BF615D200501F93 /* Terrain.json in Resources */ = {isa = PBXBuildFile; fileRef = CD958B612BF615D200501F93 /* Terrain.json */; }; - CD958B642BF6184900501F93 /* Toursprung.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD958B632BF6184900501F93 /* Toursprung.swift */; }; CD958B682BF63B3500501F93 /* MapboxNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CD958B672BF63B3500501F93 /* MapboxNavigation */; }; /* End PBXBuildFile section */ @@ -21,12 +18,9 @@ CD958B472BF6156100501F93 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; CD958B4A2BF6156100501F93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CD958B4C2BF6156100501F93 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - CD958B4E2BF6156100501F93 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; CD958B532BF6156200501F93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CD958B562BF6156200501F93 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; CD958B582BF6156200501F93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - CD958B612BF615D200501F93 /* Terrain.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Terrain.json; sourceTree = ""; }; - CD958B632BF6184900501F93 /* Toursprung.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toursprung.swift; sourceTree = ""; }; CD958B652BF63A6700501F93 /* maplibre-navigation-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "maplibre-navigation-ios"; path = ..; sourceTree = ""; }; CD958B692BF651F400501F93 /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -67,10 +61,7 @@ children = ( CD958B4A2BF6156100501F93 /* AppDelegate.swift */, CD958B4C2BF6156100501F93 /* SceneDelegate.swift */, - CD958B4E2BF6156100501F93 /* ViewController.swift */, CD958B692BF651F400501F93 /* Secrets.xcconfig */, - CD958B632BF6184900501F93 /* Toursprung.swift */, - CD958B612BF615D200501F93 /* Terrain.json */, CD958B532BF6156200501F93 /* Assets.xcassets */, CD958B552BF6156200501F93 /* LaunchScreen.storyboard */, CD958B582BF6156200501F93 /* Info.plist */, @@ -149,7 +140,6 @@ buildActionMask = 2147483647; files = ( CD958B542BF6156200501F93 /* Assets.xcassets in Resources */, - CD958B622BF615D200501F93 /* Terrain.json in Resources */, CD958B572BF6156200501F93 /* Base in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -161,8 +151,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CD958B642BF6184900501F93 /* Toursprung.swift in Sources */, - CD958B4F2BF6156100501F93 /* ViewController.swift in Sources */, CD958B4B2BF6156100501F93 /* AppDelegate.swift in Sources */, CD958B4D2BF6156100501F93 /* SceneDelegate.swift in Sources */, ); diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index b61b3afed..6dd5460e7 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -12,8 +12,6 @@ import MapLibre import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - private let styleURL = Bundle.main.url(forResource: "Terrain", withExtension: "json")! // swiftlint:disable:this force_unwrapping - var window: UIWindow? var viewController: NavigationViewController! var route: Route! diff --git a/Example/example/Terrain.json b/Example/example/Terrain.json deleted file mode 100644 index 0e4e75cd6..000000000 --- a/Example/example/Terrain.json +++ /dev/null @@ -1 +0,0 @@ -{"version":8,"name":"Terrain","metadata":{"mtk:formatStyle":true,"mtk:preload_source":{"tileSize":256,"tiles":["https://vtc-cdn.maptoolkit.net/rtc/toursprung-terrain/{z}/{x}/{y}.png?api_key="],"type":"raster"},"mtk:user":"hudhud","rapidapi":true},"light":{"anchor":"viewport","color":"white","intensity":0.1},"center":[13.255,47.723],"zoom":7,"sprite":"https://static.maptoolkit.net/sprites/hudhud","glyphs":"https://static.maptoolkit.net/fonts/{fontstack}/{range}.pbf","sources":{"hillshading":{"type":"raster","url":"https://vtc-cdn.maptoolkit.net/hillshading.json?api_key="},"mtk":{"type":"vector","url":"https://vtc-cdn.maptoolkit.net/mtk-contours-bathymetry.json?api_key="},"naturalearth":{"type":"raster","url":"https://vdata.maptoolkit.net/toursprung/naturalearth.json"}},"layers":[{"id":"background_","type":"background","minzoom":0,"maxzoom":22,"layout":{"visibility":"visible"},"paint":{"background-color":{"base":1,"stops":[[11,"#f7f9ee"],[14,"#fefefa"]]}}},{"id":"naturalearth","type":"raster","source":"naturalearth","minzoom":0,"maxzoom":7,"layout":{"visibility":"visible"},"paint":{"raster-opacity":{"base":1.5,"stops":[[5,0.6],[7,0.1]]}}},{"id":"greenland","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":5,"maxzoom":10,"filter":["all",["in","type","wood","farmland","grass"]],"layout":{"visibility":"visible"},"paint":{"fill-antialias":false,"fill-color":"#a3cf61","fill-opacity":{"base":1.2,"stops":[[6,0.15],[9,0.3],[10,0.1]]}}},{"id":"landuse_builtup_residential","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Buildings"},"minzoom":5,"maxzoom":22,"filter":["==","type","residential"],"layout":{"visibility":"visible"},"paint":{"fill-color":{"stops":[[12.9,"#e8e8e8"],[13,"#F4F4F4"]]},"fill-opacity":{"base":1.4,"stops":[[5,0.3],[9,0.8]]}}},{"id":"natural_wetland","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":8,"maxzoom":22,"filter":["==","type","wetland"],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[10,0.9],[11,1]]},"fill-pattern":"wetland"}},{"id":"landuse_park","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Park"},"minzoom":5,"maxzoom":22,"filter":["in","type","park"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#c7e78d","fill-opacity":0.9}},{"id":"landuse_cemetery","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Landuse"},"minzoom":9,"maxzoom":22,"filter":["==","type","cemetery"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#cff092"}},{"id":"landuse_hospital","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Landuse"},"minzoom":11,"maxzoom":22,"filter":["in","type","hospital","college"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#e8e3d9"}},{"id":"landuse_school","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Landuse"},"minzoom":14,"maxzoom":22,"filter":["==","type","school"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#e8e3d9"}},{"id":"natural_wood","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Wood"},"minzoom":9,"maxzoom":22,"filter":["all",["==","type","wood"]],"layout":{"visibility":"visible"},"paint":{"fill-antialias":false,"fill-color":"#a3cf61","fill-opacity":0.6}},{"id":"natural_grass","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Grass"},"minzoom":9,"maxzoom":22,"filter":["all",["in","type","grass","farmland"],["!=","subtype","park"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#ebf9c2","fill-opacity":0.7}},{"id":"poi_label_garden","type":"fill","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Landuse"},"minzoom":11,"maxzoom":22,"filter":["==","type","garden"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#ddecb1","fill-opacity":1}},{"id":"natural_glacier","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":9,"maxzoom":22,"filter":["==","subtype","glacier"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#e1ecf2","fill-opacity":{"base":1,"stops":[[9,0.1],[10,1]]}}},{"id":"natural_sand","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":5,"maxzoom":22,"filter":["==","type","sand"],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(60, 46%, 87%)"}},{"id":"natural_pitch","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["==","subtype","pitch"],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(100, 57%, 72%)","fill-opacity":0.2}},{"id":"poi_label_pitch","type":"fill","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["in","type","stadium","sports_centre","athletics"],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(100, 57%, 72%)","fill-opacity":0.2}},{"id":"natural_bare_rock","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["==","subtype","bare_rock"],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[15,0.6],[17,0.3]]},"fill-pattern":"bare_rock"}},{"id":"natural_bare_scree","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":11,"maxzoom":22,"filter":["in","subtype","bare_scree","scree"],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[13,0.2],[14,0.3],[15,0.3],[17,0.2]]},"fill-pattern":"scree"}},{"id":"natural_nationalpark","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":6,"maxzoom":22,"filter":["in","subtype","protect_class_2"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#8bb153","fill-opacity":{"base":0.3,"stops":[[8,0.5],[11,0.6],[12,0.2]]}}},{"id":"natural_protected_area","type":"fill","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":11,"maxzoom":22,"filter":["all",["in","type","nature_reserve","protected_area"],["!=","subtype","protect_class_2"],["!=","seasonal","winter"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#8bb153","fill-opacity":{"base":0.3,"stops":[[8,0.5],[11,0.6],[12,0.2]]}}},{"id":"natural_line_protected_area","type":"line","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":12,"maxzoom":22,"filter":["all",["in","type","nature_reserve","protected_area"],["!=","subtype","protect_class_2"],["!=","seasonal","winter"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#84A94C","line-opacity":{"base":0.4,"stops":[[10,0.2],[12,0.6]]},"line-width":{"base":1,"stops":[[12,2],[14,4]]}}},{"id":"natural_line_nationalpark","type":"line","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Nature Reserve"},"minzoom":6,"maxzoom":22,"filter":["in","subtype","protect_class_2"],"layout":{"visibility":"visible"},"paint":{"line-color":"#84A94C","line-opacity":{"base":0.4,"stops":[[10,0.2],[12,1]]},"line-width":{"base":1,"stops":[[12,4],[14,6]]}}},{"id":"contours_10","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":15,"maxzoom":22,"filter":["==","divisor",10],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"stops":[[15,0.4],[16,0.6]],"base":1}}},{"id":"contours_20","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":5,"maxzoom":22,"filter":["==","divisor",20],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.2],[12,0.4],[14,0.6],[16,0.8]]}}},{"id":"contours_50","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":5,"maxzoom":22,"filter":["==","divisor",50],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.4],[12,0.6],[14,0.8],[16,1]]}}},{"id":"contours_100","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":11,"maxzoom":22,"filter":["==","divisor",100],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.6],[12,0.8],[14,1],[16,1.2]]}}},{"id":"contours_500","type":"line","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":11,"maxzoom":22,"filter":["all",["==","divisor",500],["!=","ele",0]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#e1c26e","line-opacity":{"base":1,"stops":[[16,0.5],[17,0.2]]},"line-width":{"base":1,"stops":[[11,0.8],[12,1],[14,1.2],[16,1.5]]}}},{"id":"hillshading","type":"raster","source":"hillshading","metadata":{"maptoolkit:category":"Hillshading"},"minzoom":5,"maxzoom":22,"layout":{"visibility":"visible"},"paint":{"raster-opacity":{"stops":[[6,0.25],[12,0.6],[15,0.4],[17,0.2]]}}},{"id":"natural_cliff","type":"line","source":"mtk","source-layer":"natural","metadata":{"maptoolkit:category":"Landuse"},"minzoom":13,"maxzoom":22,"filter":["==","type","cliff"],"layout":{"visibility":"visible"},"paint":{"line-opacity":{"base":1,"stops":[[13,0.5],[15,1]]},"line-pattern":"cliff","line-width":8}},{"id":"water_other","type":"line","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["all",["==","$type","LineString"],["!=","type","river"],["!=","type","stream"],["!=","type","canal"]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#b7d6ff","line-width":{"base":1.3,"stops":[[13,0.5],[20,2]]}}},{"id":"water_river","type":"line","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","river"]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#b7d6ff","line-width":{"base":1.2,"stops":[[11,0.5],[20,6]]}}},{"id":"water_stream","type":"line","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["all",["==","$type","LineString"],["in","type","stream","canal"]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#b7d6ff","line-width":{"base":1.3,"stops":[[13,0.5],[20,6]]}}},{"id":"water","type":"fill","source":"mtk","source-layer":"water","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#ADD1FF"}},{"id":"bathymetry","type":"fill","source":"mtk","source-layer":"bathymetry","metadata":{"maptoolkit:category":"Water"},"minzoom":0,"maxzoom":22,"filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"#61a6ff","fill-opacity":0.12}},{"id":"landuse_pedestrian","type":"fill","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Pedestrian"},"minzoom":12,"maxzoom":22,"filter":["all",["==","$type","Polygon"],["==","subtype","pedestrian"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#dce2e5","fill-opacity":{"base":1,"stops":[[12,0.3],[15,0.9]]}}},{"id":"landuse_aeroway_fill","type":"fill","source":"mtk","source-layer":"landuse","metadata":{"maptoolkit:category":"Airport"},"minzoom":11,"maxzoom":22,"filter":["all",["==","type","aeroway"],["==","$type","Polygon"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#f0ede9","fill-opacity":0.6}},{"id":"aeroway_runway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Airport"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","subtype","runway"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#f0ede9","line-width":{"base":1.2,"stops":[[11,3],[20,16]]}}},{"id":"road_aeroway_taxiway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Airport"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","subtype","taxiway"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#f0ede9","line-width":{"base":1.2,"stops":[[11,0.5],[20,6]]}}},{"id":"road_tunnel_motorway_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"],["==","ramp",1]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-dasharray":[0.5,0.25],"line-opacity":1,"line-width":{"base":1.2,"stops":[[12,1],[13,3],[14,4],[20,15]]}}},{"id":"road_tunnel_service_track_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","service","track"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#cfcdca","line-dasharray":[0.5,0.25],"line-width":{"base":1.2,"stops":[[15,1],[16,4],[20,11]]}}},{"id":"road_tunnel_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","ramp",1],["!=","indoor",1],["!=","type","path"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-opacity":1,"line-width":{"base":1.2,"stops":[[12,1],[13,3],[14,4],[20,15]]}}},{"id":"road_tunnel_minor_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","structure","tunnel"],["==","type","minor"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#cfcdca","line-opacity":{"stops":[[12,0],[12.5,1]]},"line-width":{"base":1.2,"stops":[[12,0.5],[13,1],[14,4],[20,15]]}}},{"id":"road_tunnel_secondary_tertiary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","secondary","tertiary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tunnel_trunk_primary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","trunk","primary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tunnel_motorway_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-dasharray":[0.5,0.25],"line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tunnel_path","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","path"]],"layout":{"visibility":"none"},"paint":{"line-color":"#cba","line-dasharray":[1.5,0.75],"line-width":{"base":1.2,"stops":[[15,1.2],[20,4]]}}},{"id":"road_tunnel_motorway_ramp","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"],["==","ramp",1]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.2,"stops":[[12.5,0],[13,1.5],[14,2.5],[20,11.5]]}}},{"id":"road_tunnel_service_track","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","service","track"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.2,"stops":[[15.5,0],[16,2],[20,7.5]]}}},{"id":"road_tunnel_link","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","ramp",1],["!=","indoor",1],["!=","type","path"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff4c6","line-width":{"base":1.2,"stops":[[12.5,0],[13,1.5],[14,2.5],[20,11.5]]}}},{"id":"road_tunnel_minor","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","structure","tunnel"],["==","type","minor"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":1,"line-width":{"base":1.2,"stops":[[13.5,0],[14,2.5],[20,11.5]]}}},{"id":"road_tunnel_secondary_tertiary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","secondary","tertiary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff4c6","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tunnel_trunk_primary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["in","type","trunk","primary"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff4c6","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tunnel_motorway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","motorway"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffdaa6","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tunnel_rail","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-opacity":0.6,"line-width":{"base":1.4,"stops":[[14,0.4],[15,0.75],[20,2]]}}},{"id":"road_tunnel_rail_hatching","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","brunnel","tunnel"],["==","type","rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-dasharray":[0.2,8],"line-opacity":0.6,"line-width":{"base":1.4,"stops":[[14.5,0],[15,3],[20,8]]}}},{"id":"road_motorway_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":7,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["!in","structure","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_track_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","track"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#a6a6a6","line-dasharray":[1,2],"line-opacity":0.9,"line-width":{"base":1.4,"stops":[[12,1],[16,4],[20,11]]}}},{"id":"road_service_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","service"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#cfcdca","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[12,0.75],[20,2]]}}},{"id":"road_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":13,"maxzoom":22,"filter":["all",["==","ramp",1],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"none"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_minor_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","minor"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[11,0.75],[20,2]]}}},{"id":"road_secondary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","secondary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[9,0.1],[10,1]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_tertiary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","tertiary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[9,0.1],[10,1]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_trunk_primary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":10,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","trunk","primary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[7,0.05],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[7,0.1],[10,1]]},"line-width":{"stops":[[10,0.75],[18,2]],"base":1.2}}},{"id":"road_motorway_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","motorway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"stops":[[10,0.75],[18,2]],"base":1.2}}},{"id":"road_path_pedestrian","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Pedestrian","mtk:states":{"hiking":{"minzoom":13,"paint":{"line-width":2}}}},"minzoom":15,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","path"],["!in","brunnel","bridge","tunnel"],["!in","subtype","platform"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#c3c3c3","line-dasharray":[2,1.5],"line-opacity":0.9,"line-width":{"base":1.2,"stops":[[15,1.5],[20,2.5]]}}},{"id":"road_via_ferrata","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Pedestrian","mtk:states":{"hiking":{"minzoom":13,"paint":{"line-width":2}}}},"minzoom":15,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","via_ferrata"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-dasharray":[0.75,1.8],"line-opacity":0.8,"line-width":{"base":1.2,"stops":[[15,1.2],[20,2.5]]}}},{"id":"road_motorway_ramp","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":7,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["!in","structure","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_track","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","track"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":0.9,"line-width":{"base":1.2,"stops":[[12,0],[16,2],[20,7.5]]}}},{"id":"road_service","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":12,"maxzoom":22,"filter":["all",["==","type","service"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_ramp","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":13,"maxzoom":22,"filter":["all",["==","ramp",1],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"none"},"paint":{"line-color":"#fff","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_minor","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":11,"maxzoom":22,"filter":["all",["==","$type","LineString"],["==","type","minor"],["!in","brunnel","bridge","tunnel"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":1,"line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_secondary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","secondary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[9,"#fff"],[22,"#fff"]]},"line-opacity":{"stops":[[8,0.1],[10,1]]},"line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_tertiary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":9,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","tertiary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":{"stops":[[8,0.1],[10,1]]},"line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_trunk_primary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":7,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["in","type","trunk","primary"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[7.9,"#fff"],[8,"#fffd8b"],[11,"#fffd00"],[22,"#fffd00"]]},"line-opacity":{"stops":[[7,0.8],[10,1]]},"line-width":{"base":1.5,"stops":[[7,1],[10,2],[18,26]]}}},{"id":"road_motorway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":6,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","motorway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[6.9,"#fff"],[7,"#ffc345"]]},"line-width":{"base":1.5,"stops":[[6,0.3],[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_rail","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","railway"],["in","subtype","rail","light_rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-width":{"base":1.4,"stops":[[14,0.4],[15,0.75],[20,2]]}}},{"id":"road_rail_hatching","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["!in","brunnel","bridge","tunnel"],["==","type","railway"],["in","subtype","rail","light_rail"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-dasharray":[0.2,8],"line-width":{"base":1.4,"stops":[[14.5,0],[15,3],[20,8]]}}},{"id":"road_bridge_motorway_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":1,"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_service_track_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","service","track"],["==","brunnel","bridge"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[12,0.75],[20,2]]}}},{"id":"road_bridge_ramp_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-opacity":1,"line-width":{"base":1.4,"stops":[[10,1.25],[12,1.25],[18,16]]}}},{"id":"road_bridge_minor_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","minor"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[13,0],[14,2],[18,18]]},"line-opacity":1,"line-width":{"base":1.5,"stops":[[12,0.75],[20,2]]}}},{"id":"road_bridge_secondary_tertiary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","secondary","tertiary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-opacity":{"base":1,"stops":[[9,0.1],[10,1]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_trunk_primary_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","trunk","primary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_motorway_casing","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Road Casings"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#bfbfbf","line-gap-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]},"line-width":{"base":1.2,"stops":[[10,0.75],[18,2]]}}},{"id":"road_bridge_path","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":16.5,"maxzoom":22,"filter":["all",["==","brunnel","bridge"],["==","type","path"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#cba","line-dasharray":[1.5,0.75],"line-width":{"base":1.2,"stops":[[15,1.2],[20,4]]}}},{"id":"road_bridge_motorway_link","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_service_track","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","service","track"],["==","brunnel","bridge"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_bridge_link","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","ramp",1],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fffd8b","line-width":{"base":1.2,"stops":[[12.5,0],[13,1.5],[14,2.5],[20,11.5]]}}},{"id":"road_bridge_minor","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","minor"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-opacity":1,"line-width":{"base":1.5,"stops":[[12.5,0.5],[14,2],[18,18]]}}},{"id":"road_bridge_secondary_tertiary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","secondary","tertiary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":{"stops":[[8,"#666"],[9,"#fff"],[22,"#fff"]]},"line-opacity":{"stops":[[8,0.1],[10,1]]},"line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_trunk_primary","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["in","type","trunk","primary"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fffd8b","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_motorway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Roads"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","motorway"],["==","brunnel","bridge"]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#ffc345","line-width":{"base":1.5,"stops":[[8.5,0.5],[10,0.75],[18,26]]}}},{"id":"road_bridge_rail","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","rail"],["==","brunnel","bridge"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-width":{"base":1.4,"stops":[[14,0.4],[15,0.75],[20,2]]}}},{"id":"road_bridge_rail_hatching","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Railway"},"minzoom":0,"maxzoom":22,"filter":["all",["==","type","rail"],["==","brunnel","bridge"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#bbb","line-dasharray":[0.2,8],"line-width":{"base":1.4,"stops":[[14.5,0],[15,3],[20,8]]}}},{"id":"road_walking_local","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Hiking","mtk:states":{"hiking":{"minzoom":13,"paint":{"line-opacity":0.7}}}},"minzoom":14,"maxzoom":22,"filter":["all",["==","network","lwn"],["!=","type","via_ferrata"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-dasharray":[1.5,0.75],"line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_walking_regional","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Hiking","mtk:states":{"hiking":{"minzoom":11,"paint":{"line-opacity":0.7}}}},"minzoom":13,"maxzoom":22,"filter":["==","network","rwn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-dasharray":[1.5,0.75],"line-opacity":0.5,"line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_hiking_international","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Hiking","mtk:states":{"hiking":{"minzoom":11,"paint":{"line-opacity":0.7}}}},"minzoom":12,"maxzoom":22,"filter":["in","network","iwn","nwn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#990000","line-opacity":0.5,"line-width":{"base":1.2,"stops":[[8,1],[20,4]]}}},{"id":"road_cycling_local","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Cycling","mtk:states":{"cycling":{"minzoom":12,"paint":{"line-opacity":0.9,"line-width":{"base":1.2,"stops":[[12,2],[20,3]]}}}}},"minzoom":14,"maxzoom":22,"filter":["in","network","lcn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#6dad3e","line-dasharray":[1.5,0.75],"line-opacity":0.7,"line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_cycling_regional","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Cycling","mtk:states":{"cycling":{"minzoom":11,"paint":{"line-opacity":0.8}}}},"minzoom":13,"maxzoom":22,"filter":["in","network","rcn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#6dad3e","line-opacity":0.7,"line-width":{"base":1.2,"stops":[[8,1],[20,4]]}}},{"id":"road_cycling_international","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Cycling","mtk:states":{"cycling":{"minzoom":10,"paint":{"line-opacity":0.8,"line-width":{"base":1.2,"stops":[[8,2],[20,4]]}}}}},"minzoom":11,"maxzoom":22,"filter":["in","network","icn","ncn"],"layout":{"visibility":"visible"},"paint":{"line-color":"#6dad3e","line-opacity":0.7,"line-width":{"base":1.2,"stops":[[8,1],[20,4]]}}},{"id":"road_transit_tram","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Public Transport"},"minzoom":16,"maxzoom":22,"filter":["all",["==","type","transit"],["==","subtype","tram"]],"layout":{"visibility":"visible"},"paint":{"line-color":"#acacac","line-opacity":0.7,"line-width":{"base":1.2,"stops":[[16,1],[20,4]]}}},{"id":"road_ferry","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Ferry"},"minzoom":11,"maxzoom":22,"filter":["in","type","ferry"],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-dasharray":[1,2],"line-width":{"base":1.2,"stops":[[10,0.3],[14,1]]}}},{"id":"admin_level_3","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",["in","admin_level",3,4],["==","maritime",0]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#9e9cab","line-dasharray":[3,1,1,1],"line-opacity":0.8,"line-width":{"base":1,"stops":[[4,0.4],[5,1],[12,3]]}}},{"id":"admin_level_2","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":0,"maxzoom":22,"filter":["all",["==","admin_level",2],["==","disputed",0],["==","maritime",0]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(230, 8%, 51%)","line-width":{"base":1,"stops":[[3,0.5],[10,2]]}}},{"id":"admin_level_2_disputed","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",["==","admin_level",2],["==","disputed",1],["==","maritime",0]],"layout":{"line-cap":"round","visibility":"visible"},"paint":{"line-color":"#9e9cab","line-dasharray":[2,2],"line-width":{"base":1,"stops":[[4,1.4],[5,2],[12,8]]}}},{"id":"admin_level_3_maritime","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",[">=","admin_level",3],["==","maritime",1]],"layout":{"line-join":"round","visibility":"visible"},"paint":{"line-color":"#a0c8f0","line-dasharray":[3,1,1,1],"line-opacity":0.5,"line-width":{"base":1,"stops":[[4,0.4],[5,1],[12,3]]}}},{"id":"admin_level_2_maritime","type":"line","source":"mtk","source-layer":"admin","metadata":{"maptoolkit:category":"Borders"},"minzoom":4,"maxzoom":22,"filter":["all",["==","admin_level",2],["==","maritime",1]],"layout":{"line-cap":"round","visibility":"none"},"paint":{"line-color":"#a0c8f0","line-opacity":0.5,"line-width":{"base":1,"stops":[[4,1.4],[5,2],[12,8]]}}},{"id":"road_aerialway","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":10,"maxzoom":22,"filter":["all",["==","type","aerialway"],["!in","subtype","t-bar","drag_lift","magic_carpet","rope_tow"]],"layout":{"line-cap":"square","line-join":"round","visibility":"visible"},"paint":{"line-color":"#666","line-opacity":{"stops":[[11,0.35],[14,0.5]]},"line-width":{"stops":[[10,0.5],[12,1.4]]}}},{"id":"road_aerialway_patterns","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":10,"maxzoom":22,"filter":["all",["==","type","aerialway"],["!in","subtype","t-bar","drag_lift","magic_carpet","rope_tow"]],"layout":{"line-cap":"square","line-join":"round","visibility":"visible"},"paint":{"line-color":"#666","line-dasharray":[1,6],"line-opacity":{"stops":[[11,0.35],[14,0.5]]},"line-width":{"stops":[[10,0.5],[12,2.8]]}}},{"id":"road_piste_easy_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-easy"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#0969B3","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,2],[13,5],[14,7],[16,9]]}}},{"id":"road_piste_easy","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-easy"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#0969B3","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_pistes_intermediate_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-intermediate"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#EF2021","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,2],[13,5],[14,7],[16,9]]}}},{"id":"road_pistes_intermediate","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-intermediate"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#EF2021","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_pistes_difficult_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-advanced"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#000","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,2],[13,5],[14,7],[16,9]]}}},{"id":"road_pistes_difficult","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-advanced"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#000","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1],[20,3]]}}},{"id":"road_pistes_nordic","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","nordic"],["==","subtype","nordic-easy"],["==","subtype","nordic-intermediate"],["==","subtype","nordic-advaned"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#606060","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1.2],[15,3],[22,2]]}}},{"id":"road_pistes_skiroutes","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","downhill-freeride"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#EF2021","line-dasharray":[3,2],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1.2],[15,3],[22,2]]}}},{"id":"road_piste_connection_outline","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","connection"],["==","subtype","connection-easy"],["==","subtype","connection-intermediate"],["==","subtype","connection-advaned"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":{"base":1,"stops":[[12,4],[13,6],[22,7]]},"line-color":"#0969B3","line-opacity":{"base":1,"stops":[[11,0.8],[14,1]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1,"stops":[[11,4],[13,7],[14,9],[22,9]]}}},{"id":"road_piste_connection","type":"line","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":14,"maxzoom":22,"filter":["all",["==","type","piste"],["==","subtype","connection"],["==","subtype","connection-easy"],["==","subtype","connection-intermediate"],["==","subtype","connection-advaned"]],"layout":{"line-cap":"square","line-join":"round","visibility":"none"},"paint":{"line-blur":0,"line-color":"#0969B3","line-dasharray":[1,1],"line-opacity":{"stops":[[11,0.3],[13,0.6]]},"line-translate":[0,0],"line-translate-anchor":"map","line-width":{"base":1.2,"stops":[[13,1.2],[15,3],[22,2]]}}},{"id":"building","type":"fill","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings"},"minzoom":13,"maxzoom":22,"filter":["any",["!has","level"],[">=","level",0]],"layout":{"visibility":"visible"},"paint":{"fill-color":{"stops":[[13,"#E8E8E8"],[15,"#E8E8E8"]]},"fill-opacity":0.5,"fill-outline-color":"#c5c5c5"}},{"id":"building_public","type":"fill","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings"},"minzoom":13,"maxzoom":22,"filter":["all",["any",["!has","level"],[">=","level",0]],["any",["in","building","church","school","public","train_station"],["in","type","library"]]],"layout":{"visibility":"visible"},"paint":{"fill-color":"#F9E7E7","fill-opacity":0.5,"fill-outline-color":"#efbebe"}},{"id":"building_3D","type":"fill-extrusion","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings","mtk:states":{"buildings3d":{"layout":{"visibility":"none"}}}},"minzoom":15,"maxzoom":22,"filter":["==","extrude",true],"layout":{"visibility":"visible"},"paint":{"fill-extrusion-base":["interpolate",["linear"],["zoom"],15,0,15.05,["get","min_height"]],"fill-extrusion-color":"#E8E8E8","fill-extrusion-height":["interpolate",["linear"],["zoom"],15,0,15.05,["get","height"]],"fill-extrusion-opacity":0.6}},{"id":"building_3D_public","type":"fill-extrusion","source":"mtk","source-layer":"building","metadata":{"maptoolkit:category":"Buildings","mtk:states":{"buildings3d":{"layout":{"visibility":"none"}}}},"minzoom":15,"maxzoom":22,"filter":["all",["==","extrude",true],["any",["in","building","church","school","public","train_station"],["in","type","library"]]],"layout":{"visibility":"visible"},"paint":{"fill-extrusion-base":["interpolate",["linear"],["zoom"],15,0,15.05,["get","min_height"]],"fill-extrusion-color":"#efbebe","fill-extrusion-height":["interpolate",["linear"],["zoom"],15,0,15.05,["get","height"]],"fill-extrusion-opacity":0.6}},{"id":"road_oneway_arrows","type":"symbol","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Oneway Arrows"},"minzoom":15,"maxzoom":22,"filter":["all",["==","oneway",1],["!=","type","transit"],["!in","cycleway","opposite","opposite_lane"]],"layout":{"icon-image":{"base":1,"stops":[[15,"oneway"]]},"icon-padding":2,"icon-rotation-alignment":"map","icon-size":0.7,"symbol-placement":"line","symbol-spacing":250,"visibility":"visible"},"paint":{"text-color":"#334"}},{"id":"road_oneway_arrows_except_cycles","type":"symbol","source":"mtk","source-layer":"road","metadata":{"maptoolkit:category":"Oneway Arrows"},"minzoom":15,"maxzoom":22,"filter":["all",["==","oneway",1],["!=","type","transit"],["in","cycleway","opposite","opposite_lane"]],"layout":{"icon-image":{"base":1,"stops":[[15,"oneway-except-cycles"]]},"icon-padding":2,"icon-rotation-alignment":"map","icon-size":0.7,"symbol-placement":"line","symbol-spacing":250,"visibility":"visible"},"paint":{"text-color":"#334"}},{"id":"housenum_label","type":"symbol","source":"mtk","source-layer":"housenum_label","metadata":{"maptoolkit:category":"House Numbers","maptoolkit:type":"label"},"minzoom":17,"maxzoom":22,"layout":{"text-field":"{housenumber}","text-font":["Noto Sans Regular"],"text-max-width":7,"text-pitch-alignment":"viewport","text-size":9.5,"visibility":"visible"},"paint":{"text-color":"hsl(35, 2%, 69%)","text-halo-color":"hsl(35, 8%, 85%)","text-halo-width":0.5,"text-opacity":{"base":1,"stops":[[16,0],[16.5,1]]}}},{"id":"contours_100_label","type":"symbol","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":14,"maxzoom":22,"filter":["==","divisor",100],"layout":{"symbol-placement":"line","symbol-spacing":200,"text-field":"{ele}","text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":8,"visibility":"visible"},"paint":{"text-color":"#e1c26e"}},{"id":"contours_500_label","type":"symbol","source":"mtk","source-layer":"contours","metadata":{"maptoolkit:category":"Contour Lines"},"minzoom":13,"maxzoom":22,"filter":["all",["==","divisor",500],["!=","ele",0]],"layout":{"symbol-placement":"line","symbol-spacing":200,"text-field":"{ele}","text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":10,"visibility":"visible"},"paint":{"text-color":"#e1c26e"}},{"id":"poi_label_3","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":17,"maxzoom":22,"filter":["all",["!in","subtype","subway_entrance","board","map","guidepost","drag_lift","station","pitch"],["!in","type","information"]],"layout":{"icon-image":"{type}-11","symbol-avoid-edges":false,"text-pitch-alignment":"viewport","visibility":"visible"},"paint":{"icon-opacity":0.4}},{"id":"poi_label_2","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":15,"maxzoom":22,"filter":["all",["<=","rank",50],["!in","subtype","subway_entrance","board","map","guidepost","drag_lift","station","pitch"],["!in","type","information"]],"layout":{"icon-image":"{type}-11","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":2,"text-pitch-alignment":"viewport","text-size":12,"visibility":"visible"},"paint":{"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"poi_label_1","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["all",["<=","rank",8],["!in","subtype","subway_entrance","board","map","guidepost","drag_lift","station","pitch","dog_park","playground","garden","picnic_site"],["!in","type","information"]],"layout":{"icon-image":"{type}-11","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":{"base":1,"stops":[[14,20],[20,2]]},"text-pitch-alignment":"viewport","text-size":10,"visibility":"visible"},"paint":{"icon-opacity":0.6,"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"hsla(0, 0%, 100%, 0.8)","text-halo-width":0.9}},{"id":"poi_toilet","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":17,"maxzoom":22,"filter":["==","type","toilets"],"layout":{"icon-image":"toilet","symbol-avoid-edges":false,"visibility":"visible"},"paint":{"icon-opacity":0.8}},{"id":"poi_generator","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":13,"maxzoom":22,"filter":["==","subtype","wind"],"layout":{"icon-image":"generator_wind","icon-size":1,"symbol-avoid-edges":false,"visibility":"visible"},"paint":{"icon-opacity":0.6}},{"id":"poi_label_bank","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["==","type","bank"],"layout":{"icon-image":"{type}","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":{"base":1,"stops":[[14,20],[20,2]]},"text-pitch-alignment":"viewport","text-size":10,"visibility":"visible"},"paint":{"icon-opacity":0.6,"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"poi_label_rail_station","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":15,"maxzoom":22,"filter":["all",["==","type","station"],["!in","subtype","t-bar","magic_carpet","rope_tow","drag_lift","gondola","cable_car","chair_lift"]],"layout":{"icon-image":"{maki}-11","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":{"base":1,"stops":[[14,20],[20,2]]},"text-pitch-alignment":"viewport","text-size":12,"visibility":"visible"},"paint":{"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"poi_label_aerialway_station_icon","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":10,"maxzoom":22,"filter":["all",["==","type","station"],["in","subtype","gondola","cable_car","chair_lift"],["!in","subtype","t-bar","magic_carpet","rope_tow","drag_lift"]],"layout":{"icon-image":"{subtype}","symbol-avoid-edges":false,"text-pitch-alignment":"viewport","visibility":"visible"}},{"id":"road_label_walking_shied","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Hiking"},"minzoom":12,"maxzoom":22,"filter":["all",["in","network","iwn"],["has","ref"]],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","icon-size":0.7,"symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":8,"visibility":"visible"},"paint":{"text-color":"#990000"}},{"id":"road_label_cycling_shied","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Cycling","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["all",["in","network","icn"],["has","ref"]],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","icon-size":0.7,"symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":8,"visibility":"visible"},"paint":{"text-color":"#6dad3e"}},{"id":"place_label_other","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["in","type","neighbourhood","valley","glacier","locality","wetland","cliff","wineyard","ridge","wood","bev_ried","bev_gebiet"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,10],[15,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 33%, 1)","text-halo-color":"hsla(0, 0%, 100%, 0.6)","text-halo-width":1.1}},{"id":"poi_label_peaks","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Peaks","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["==","type","peak"],"layout":{"icon-image":"triangle-15","icon-size":0.4,"text-anchor":"top","text-field":"{name}\n{ele}","text-font":["Noto Sans Regular"],"text-offset":[0,0.5],"text-optional":false,"text-padding":{"base":1,"stops":[[9,35],[12,15]]},"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,15]]},"visibility":"visible"},"paint":{"text-color":"#333","text-opacity":0.9,"icon-opacity":0.8}},{"id":"road_label_pistes","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Mountain Sports"},"minzoom":13,"maxzoom":22,"filter":["==","type","piste"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":400,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.05,"text-max-angle":40,"text-padding":2,"text-pitch-alignment":"viewport","text-size":{"stops":[[8,8],[20,20]],"base":1.2},"visibility":"none"},"paint":{"text-color":"hsla(0, 0%, 0%, 0.8)","text-halo-color":"hsla(0, 0%, 100%, 1)","text-halo-width":{"stops":[[13,1],[20,2]]}}},{"id":"poi_label_mountain_pass","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Peaks"},"minzoom":11,"maxzoom":22,"filter":["==","type","moutain_pass"],"layout":{"icon-image":"triangle-stroked-15","icon-padding":20,"icon-size":1,"symbol-avoid-edges":false,"text-anchor":"top","text-field":"{name}\n{ele}","text-font":["Noto Sans Regular"],"text-offset":[0,0.5],"text-optional":false,"text-padding":20,"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,13]]},"visibility":"visible"},"paint":{"text-color":"#333"}},{"id":"poi_label_alpine_hut","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"Peaks","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["==","subtype","alpine_hut"],"layout":{"icon-image":"alpine_hut","icon-padding":20,"icon-size":1,"symbol-avoid-edges":false,"text-anchor":"top","text-field":"{name}\n{ele}","text-font":["Noto Sans Regular"],"text-offset":[0,0.5],"text-optional":false,"text-padding":20,"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,13]]},"visibility":"visible"},"paint":{"text-color":"#333"}},{"id":"road_label_walking","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Labels","maptoolkit:type":"label"},"minzoom":13,"maxzoom":22,"filter":["in","network","iwn","nwn","rwn"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,8],[20,14]]},"visibility":"visible"},"paint":{"text-color":"#990000","text-halo-blur":0.5,"text-halo-color":"hsla(0, 0%, 100%, 0.8)","text-halo-width":0.9}},{"id":"road_label_cycling","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cycling"},"minzoom":11,"maxzoom":22,"filter":["in","network","icn","ncn","rcn"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,8],[20,14]]},"visibility":"visible"},"paint":{"text-color":"#6dad3e","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"road_label_2","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["all",["!in","type","ferry","motorway","primary","secondary","piste"],["!in","subtype","t-bar","drag_lift","magic_carpet","rope_tow","downhill","nordic"],["!has","network"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":200,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[10,8],[16,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 0%, 0.6)","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"road_label_1","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Labels","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":8,"maxzoom":22,"filter":["all",["in","type","motorway","primary","secondary"],["!in","network","ncn","rcn","icn","lcn","iwn","nwn","rwn","lwn"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[13,10],[16,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 0%, 0.7)","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"road_label_highway_shield_primary","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Shields","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["==","type","primary"],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":10,"visibility":"visible"}},{"id":"road_label_highway_shield_motorway","type":"symbol","source":"mtk","source-layer":"road_label","metadata":{"maptoolkit:category":"Road Shields","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["==","type","motorway"],"layout":{"icon-image":"motorway_{ref_length}","icon-rotation-alignment":"viewport","icon-size":1,"symbol-avoid-edges":false,"symbol-placement":{"base":1,"stops":[[10,"point"],[11,"line"]]},"symbol-spacing":500,"text-field":"{ref}","text-font":["Open Sans Bold"],"text-pitch-alignment":"viewport","text-rotation-alignment":"viewport","text-size":10,"visibility":"visible"}},{"id":"place_label_hamlet_suburb","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":11,"maxzoom":22,"filter":["in","type","hamlet","suburb"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,10],[15,14]]},"visibility":"visible"},"paint":{"text-color":"hsla(0, 0%, 33%, 1)","text-halo-color":"hsla(0, 0%, 100%, 0.6)","text-halo-width":1.1}},{"id":"poi_label_subway_label","type":"symbol","source":"mtk","source-layer":"poi_label","metadata":{"maptoolkit:category":"POI Labels","maptoolkit:type":"label"},"minzoom":14,"maxzoom":22,"filter":["all",["==","subtype","subway"],["==","agg_stop",1]],"layout":{"icon-image":"rail_metro","symbol-avoid-edges":false,"text-anchor":"top","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":9,"text-offset":[0,0.6],"text-padding":2,"text-pitch-alignment":"viewport","text-size":12,"visibility":"visible"},"paint":{"text-color":"#666","text-halo-blur":0.5,"text-halo-color":"#ffffff","text-halo-width":1}},{"id":"place_label_piste","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Villages","maptoolkit:type":"label"},"minzoom":8,"maxzoom":14,"filter":["==","type","piste"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":8,"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,12],[14,18]]},"visibility":"none"},"paint":{"text-color":"#0069a3","text-halo-color":"hsl(0, 0%, 100%)","text-halo-width":1,"text-opacity":0.6}},{"id":"place_label_village","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Villages","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":8,"maxzoom":22,"filter":["==","type","village"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":8,"text-padding":{"base":1,"stops":[[8,10],[11,2]]},"text-pitch-alignment":"viewport","text-size":{"base":1,"stops":[[8,9.5],[16,18]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[8,"hsla(0, 0%, 33%, 1)"],[16,"hsla(0, 0%, 0%, 1)"]]},"text-halo-color":"hsla(0, 0%, 100%, 0.7)","text-halo-width":1}},{"id":"water_label_lakeline","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":3,"maxzoom":22,"filter":["all",["in","type","water","lake"],["==","$type","LineString"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.2,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[9,12],[20,18]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"water_label_point","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":3,"maxzoom":22,"filter":["all",["in","type","water","lake"],["==","$type","Point"],["<=","rank",15]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"point","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.2,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[9,10],[20,18]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"water_label_stream","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label"},"minzoom":12,"maxzoom":22,"filter":["all",["==","$type","LineString"],["in","type","stream"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":350,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[12,8],[18,14]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"water_label_river","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label"},"minzoom":12,"maxzoom":22,"filter":["all",["==","$type","LineString"],["in","type","river"]],"layout":{"symbol-avoid-edges":false,"symbol-placement":"line","symbol-spacing":350,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.1,"text-line-height":1.6,"text-max-width":5,"text-pitch-alignment":"viewport","text-size":{"stops":[[3,10],[13,14],[18,20]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"hsla(0, 0%, 100%, 0.7)","text-halo-width":1.4}},{"id":"place_label_town_small","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Towns"},"minzoom":6,"maxzoom":22,"filter":["all",["==","type","town"],[">","rank",5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-padding":0,"icon-size":0.4,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[7,["Noto Sans Regular"]],[12,["Noto Sans Bold"]],[22,["Noto Sans Bold"]]]},"text-line-height":{"base":1,"stops":[[8,1],[20,1.3]]},"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-padding":{"base":1,"stops":[[6,10],[10,2]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[6,11],[12,15],[19,32],[20,48]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[7,"hsl(0, 0%, 0%)"],[18.9,"hsl(0, 0%, 0%)"],[19,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[18.9,"hsla(0, 0%, 100%, 0.6)"],[19,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[18.9,1],[19,0.5]]}}},{"id":"place_label_town_large","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Towns"},"minzoom":5,"maxzoom":22,"filter":["all",["==","type","town"],["<=","rank",5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-padding":0,"icon-size":0.4,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[6,["Noto Sans Regular"]],[11,["Noto Sans Bold"]],[22,["Noto Sans Bold"]]]},"text-line-height":{"base":1,"stops":[[8,1],[20,1.3]]},"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[6,12],[11,16],[18,32],[19,48],[19.9,48],[20,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[6,"hsl(0, 0%, 0%)"],[17.9,"hsl(0, 0%, 0%)"],[18,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[6,"hsla(0, 0%, 100%, 0.5)"],[17.9,"hsla(0, 0%, 100%, 0.5)"],[18,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[6,1],[17.9,1],[18,0.5]]}}},{"id":"place_label_island_small","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label"},"minzoom":8,"maxzoom":22,"filter":["all",["==","type","island"],[">","rank",3]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,12],[15,16]]},"visibility":"visible"},"paint":{"text-color":"#666","text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.2}},{"id":"place_label_island_large","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Other Places","maptoolkit:type":"label"},"minzoom":7,"maxzoom":22,"filter":["all",["==","type","island"],["<","rank",4]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.1,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[12,12],[15,16]]},"visibility":"visible"},"paint":{"text-color":"#666","text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.2}},{"id":"place_label_city_small","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cities"},"minzoom":4,"maxzoom":15.9,"filter":["all",["==","type","city"],["!in","rank",0,1,2,3,4,5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-size":0.5,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[0,["Noto Sans Regular"]],[9,["Noto Sans Bold"]]]},"text-line-height":1.1,"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,12],[8,15],[9,18],[15,32],[16,48],[16.9,48],[17,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[0,"hsl(0, 0%, 0%)"],[13.9,"hsl(0, 0%, 0%)"],[14,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[5,"hsla(0, 0%, 100%, 0.5)"],[13,"hsla(0, 0%, 100%, 0.5)"],[14,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[5,1],[14,1],[15,0.5]]}}},{"id":"place_label_city_medium","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cities"},"minzoom":4,"maxzoom":15.9,"filter":["all",["==","type","city"],["in","rank",3,4,5]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-size":0.5,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":{"base":1,"stops":[[0,["Noto Sans Regular"]],[9,["Noto Sans Bold"]]]},"text-line-height":1.1,"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,15],[7,18],[9,22],[14,48],[15,64],[15.9,64],[16,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[0,"hsl(0, 0%, 0%)"],[13.9,"hsl(0, 0%, 0%)"],[14,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[5,"hsla(0, 0%, 100%, 0.5)"],[13.9,"hsla(0, 0%, 100%, 0.5)"],[14,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[5,1],[13.9,1],[14,0.5]]}}},{"id":"natural_label_mountain_range","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Mountain Ranges","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":5,"maxzoom":12,"filter":["in","type","mountain_range"],"layout":{"symbol-placement":"line","text-field":"{name}","text-font":["Noto Sans Regular"],"text-letter-spacing":0.5,"text-max-width":100,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,8],[11,16]]},"visibility":"visible"},"paint":{"text-color":"hsl(0, 0%, 20%)","text-halo-color":"hsl(0, 0%, 100%)","text-halo-width":1.1}},{"id":"natural_label_protected_area","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Nature Reserve","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}}}},"minzoom":9,"maxzoom":22,"filter":["all",["in","type","nature_reserve","protected_area"],["!=","subtype","protect_class_2"],["!=","seasonal","winter"]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.2,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":10,"stops":[[12,10],[15,13]]},"visibility":"visible"},"paint":{"text-color":"#425327","text-halo-color":"hsla(0, 0%, 100%, 0.6)","text-halo-width":1.1}},{"id":"natural_label_nationalpark","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Nature Reserve","maptoolkit:type":"label","mtk:states":{"minimap":{"layout":{"visibility":"none"}},"nationalparks":{"layout":{"text-size":{"base":1.2,"stops":[[12,12],[15,16]]}}}}},"minzoom":5,"maxzoom":22,"filter":["in","type","national_park"],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-letter-spacing":0.2,"text-max-width":9,"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[10,12],[15,16]]},"visibility":"visible"},"paint":{"text-color":"#425327","text-halo-color":"hsla(0, 0%, 100%, 0.9)","text-halo-width":1.1}},{"id":"place_label_city_large","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:type":"label","maptoolkit:category":"Cities"},"minzoom":4,"maxzoom":15.9,"filter":["all",["==","type","city"],["in","rank",0,1,2]],"layout":{"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"icon-offset":[0,5],"icon-size":0.6,"symbol-avoid-edges":false,"text-anchor":{"base":1,"stops":[[7.99,"bottom"],[8,"center"]]},"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Bold"],"text-max-width":8,"text-offset":{"base":1,"stops":[[7.99,[0,-0.2]],[8,[0,-0.2]]]},"text-padding":{"base":1,"stops":[[7,20],[8,5]]},"text-pitch-alignment":"viewport","text-size":{"base":1.2,"stops":[[5,15],[9,24],[13,48],[15,64],[17.9,64],[18,0]]},"visibility":"visible"},"paint":{"text-color":{"base":1,"stops":[[0,"hsl(0, 0%, 0%)"],[13.5,"hsl(0, 0%, 0%)"],[14,"hsla(0, 0%, 100%, 0.4)"]]},"text-halo-color":{"base":1,"stops":[[5,"hsla(0, 0%, 100%, 0.5)"],[13.5,"hsla(0, 0%, 100%, 0.5)"],[14,"hsla(0, 0%, 2%, 0.4)"]]},"text-halo-width":{"base":1.4,"stops":[[5,1],[13.5,1],[14,0.5]]}}},{"id":"water_label_ocean","type":"symbol","source":"mtk","source-layer":"water_label","metadata":{"maptoolkit:category":"Water Labels","maptoolkit:type":"label"},"minzoom":1,"maxzoom":22,"filter":["in","type","sea","ocean"],"layout":{"symbol-avoid-edges":false,"symbol-placement":"point","text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Italic"],"text-letter-spacing":0.2,"text-line-height":1.6,"text-max-width":5,"text-offset":[0,1],"text-pitch-alignment":"viewport","text-size":{"stops":[[3,12],[4,16]]},"visibility":"visible"},"paint":{"text-color":"#74aee9","text-halo-color":"rgba(255,255,255,0.7)","text-halo-width":1.5}},{"id":"place_label_country_4","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":3,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",4]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[4,11],[6,15]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}},{"id":"place_label_country_3","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":3,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",3]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[3,11],[7,17]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}},{"id":"place_label_country_2","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":2,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",2]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[2,11],[5,18]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}},{"id":"place_label_country_1","type":"symbol","source":"mtk","source-layer":"place_label","metadata":{"maptoolkit:category":"Country Labels","maptoolkit:type":"label"},"minzoom":1,"maxzoom":22,"filter":["all",["==","type","country"],["==","rank",1]],"layout":{"symbol-avoid-edges":false,"text-field":["case",["has","is_nonlatin"],["concat",["get","name"],"\n",["get","name_en"]],["get","name"]],"text-font":["Noto Sans Regular"],"text-max-width":6.25,"text-pitch-alignment":"viewport","text-size":{"stops":[[1,11],[4,17]]},"text-transform":"uppercase","visibility":"visible"},"paint":{"text-color":"#334","text-halo-blur":1,"text-halo-color":"rgba(255,255,255,0.8)","text-halo-width":1.4}}]} \ No newline at end of file diff --git a/Example/example/Toursprung.swift b/Example/example/Toursprung.swift deleted file mode 100644 index 54a2a9fad..000000000 --- a/Example/example/Toursprung.swift +++ /dev/null @@ -1,302 +0,0 @@ -// -// Toursprung.swift -// Navigation -// -// Created by Patrick Kladek on 06.03.24. -// - -import Foundation -import MapboxCoreNavigation -import MapboxDirections -import MapboxNavigation -import MapLibre -import OSLog - -public typealias JSONDictionary = [String: Any] - -// MARK: - Toursprung - -public class Toursprung { - public enum ToursprungError: LocalizedError, Equatable { - case invalidUrl(message: String?) - case invalidResponse(message: String?) - case noRoute(message: String?) - case noSegment(message: String?) - case forbidden(message: String?) - case invalidInput(message: String?) - case profileNotFound(message: String?) - case notAuthorized(message: String?) - - public var errorDescription: String? { - switch self { - case let .invalidUrl(message): - errorDescription(message: message, defaultMessage: "Calculating route failed") - case let .invalidResponse(message): - errorDescription(message: message, defaultMessage: "Calculating route failed") - case let .noRoute(message): - errorDescription(message: message, defaultMessage: "No route found.") - case let .noSegment(message): - errorDescription(message: message, defaultMessage: "No segment found.") - case let .forbidden(message): - errorDescription(message: message, defaultMessage: "Forbidden access.") - case let .invalidInput(message): - errorDescription(message: message, defaultMessage: "Invalid input.") - case let .profileNotFound(message: message): - errorDescription(message: message, defaultMessage: "ProfileNotFound") - case let .notAuthorized(message: message): - errorDescription(message: message, defaultMessage: "NotAuthorized") - } - } - - public var failureReason: String? { - switch self { - case let .invalidUrl(message): - self.errorDescription(message: message, defaultMessage: "Calculating route failed because url can't be created") - case let .invalidResponse(message): - self.errorDescription(message: message, defaultMessage: "Hudhud responded with invalid route") - case let .noRoute(message): - self.errorDescription(message: message, defaultMessage: "No route found.") - case let .noSegment(message): - self.errorDescription(message: message, defaultMessage: "No segment found.") - case let .forbidden(message): - self.errorDescription(message: message, defaultMessage: "Forbidden access.") - case let .invalidInput(message): - self.errorDescription(message: message, defaultMessage: "Invalid input.") - case let .profileNotFound(message: message): - self.errorDescription(message: message, defaultMessage: "Profile Not Found") - case let .notAuthorized(message: message): - self.errorDescription(message: message, defaultMessage: "Not Authorized") - } - } - - public var recoverySuggestion: String? { - switch self { - case .invalidUrl: - "Retry with another destination" - case .invalidResponse: - "Update the app or retry with another destination" - case .noRoute: - "Retry with another destination" - case .noSegment: - "Retry with another destination" - case .forbidden: - "Forbidden access." - case .invalidInput: - "Invalid input." - case .profileNotFound: - "Profile Not Found" - case .notAuthorized: - "Not Authorized" - } - } - - public var helpAnchor: String? { - switch self { - case .invalidUrl: - "Search for another location and start navigation to there" - case .invalidResponse: - "Go to the AppStore and download the newest version of the App. Alternatively search for another location and start navigation to there." - case .noRoute: - "Search for another location and start navigation to there" - case .noSegment: - "Search for another location and start navigation to there" - case .forbidden: - "Forbidden access" - case .invalidInput: - "Invalid input" - case .profileNotFound: - "Profile Not Found" - case .notAuthorized: - "Not Authorized" - } - } - - // MARK: - Private - - private func errorDescription(message: String?, defaultMessage: String) -> String { - var description = defaultMessage - if let message { - description += " \(message)" - } - return description - } - } - - public typealias RouteCompletionHandler = (_ waypoints: [Waypoint]?, _ routes: [Route]?, _ error: Error?) -> Void - - public static let shared = Toursprung() - - // MARK: - Lifecycle - - public init() {} - - // MARK: - Public - - public struct RouteCalculationResult { - public let waypoints: [Waypoint] - public let routes: [Route] - } - - @discardableResult - public func calculate(_ options: RouteOptions) async throws -> RouteCalculationResult { - let url = try options.url - let answer: (data: Data, response: URLResponse) = try await URLSession.shared.data(from: url) - let json: JSONDictionary - - guard answer.response.mimeType == "application/json" else { - throw ToursprungError.invalidResponse(message: "MIME Type not matching application/json") - } - - do { - json = try JSONSerialization.jsonObject(with: answer.data, options: []) as? [String: Any] ?? [:] - } catch let error as ToursprungError { - throw ToursprungError.invalidResponse(message: "Route error occurred: \(error.localizedDescription)") - } - - let apiStatusCode = json["code"] as? String - let apiMessage = json["message"] as? String - guard (apiStatusCode == nil && apiMessage == nil) || apiStatusCode == "Ok" else { - switch apiStatusCode { - case "InvalidInput": - throw ToursprungError.invalidInput(message: apiMessage) - case "Not Authorized - No Token": - throw ToursprungError.notAuthorized(message: apiMessage) - case "Not Authorized - Invalid Token": - throw ToursprungError.notAuthorized(message: apiMessage) - case "Forbidden": - throw ToursprungError.forbidden(message: apiMessage) - case "ProfileNotFound": - throw ToursprungError.profileNotFound(message: apiMessage) - case "NoSegment": - throw ToursprungError.noSegment(message: apiMessage) - case "NoRoute": - throw ToursprungError.noRoute(message: apiMessage) - default: - throw ToursprungError.invalidResponse(message: nil) - } - } - - let response = try options.response(from: json) - for route in response.routes { - route.routeIdentifier = json["uuid"] as? String - } - guard let httpResponse = answer.response as? HTTPURLResponse else { - throw ToursprungError.invalidResponse(message: "Unexpected response type") - } - let httpStatusCode = httpResponse.statusCode - switch httpStatusCode { - case 500 ... 599: - throw ToursprungError.invalidResponse(message: "Server error HTTP status code: \(httpStatusCode)") - case 200 ... 299: - return RouteCalculationResult(waypoints: response.waypoint, routes: response.routes) - default: - throw ToursprungError.invalidResponse(message: "Server error occurred") - } - } -} - -// MARK: - Private - -private extension RouteOptions { - var url: URL { - get throws { - let stops = self.waypoints.map { "\($0.coordinate.longitude),\($0.coordinate.latitude)" }.joined(separator: ";") - - var components = URLComponents() - components.scheme = "https" - components.host = "gh.maptoolkit.net" - components.path = "/navigate/directions/v5/gh/car/\(stops)" - components.queryItems = [ - URLQueryItem(name: "access_token", value: ""), - URLQueryItem(name: "alternatives", value: "false"), - URLQueryItem(name: "geometries", value: "polyline6"), - URLQueryItem(name: "overview", value: "full"), - URLQueryItem(name: "steps", value: "true"), - URLQueryItem(name: "continue_straight", value: "true"), - URLQueryItem(name: "annotations", value: "congestion,distance"), - URLQueryItem(name: "language", value: Locale.preferredLanguages.first ?? "en-US"), - URLQueryItem(name: "roundabout_exits", value: "true"), - URLQueryItem(name: "voice_instructions", value: "true"), - URLQueryItem(name: "banner_instructions", value: "true"), - URLQueryItem(name: "voice_units", value: "metric") - ] - guard let url = components.url else { - throw Toursprung.ToursprungError.invalidUrl(message: "Couldn't create url from URLComponents") - } - - return url - } - } - - func response(from json: JSONDictionary) throws -> (waypoint: [Waypoint], routes: [Route]) { - var namedWaypoints: [Waypoint] = [] - if let jsonWaypoints = (json["waypoints"] as? [JSONDictionary]) { - namedWaypoints = try zip(jsonWaypoints, self.waypoints).compactMap { api, local -> Waypoint? in - guard let location = api["location"] as? [Double] else { - return nil - } - - let coordinate = try CLLocationCoordinate2D(geoJSON: location) - let possibleAPIName = api["name"] as? String - let apiName = possibleAPIName?.nonEmptyString - return Waypoint(coordinate: coordinate, name: local.name ?? apiName) - } - } - - let routes = (json["routes"] as? [JSONDictionary] ?? []).compactMap { - Route(json: $0, waypoints: waypoints, options: self) - } - return (namedWaypoints, routes) - } -} - -public extension CLLocationCoordinate2D { - enum GeoJSONError: LocalizedError { - case invalidCoordinates - case invalidType - - public var errorDescription: String? { - switch self { - case .invalidCoordinates: - "Can not read coordinates" - case .invalidType: - "Expecting different GeoJSON type" - } - } - - public var failureReason: String? { - switch self { - case .invalidCoordinates: - "data has more or less then 2 coordinates, expecting exactly 2" - case .invalidType: - "type should be either LineString or Point" - } - } - } - - init(geoJSON array: [Double]) throws { - guard array.count == 2 else { - throw GeoJSONError.invalidCoordinates - } - - self.init(latitude: array[1], longitude: array[0]) - } - - init(geoJSON point: JSONDictionary) throws { - guard point["type"] as? String == "Point" else { - throw GeoJSONError.invalidType - } - - try self.init(geoJSON: point["coordinates"] as? [Double] ?? []) - } - - static func coordinates(geoJSON lineString: JSONDictionary) throws -> [CLLocationCoordinate2D] { - let type = lineString["type"] as? String - guard type == "LineString" || type == "Point" else { - throw GeoJSONError.invalidType - } - - let coordinates = lineString["coordinates"] as? [[Double]] ?? [] - return try coordinates.map { try self.init(geoJSON: $0) } - } -} diff --git a/Example/example/ViewController.swift b/Example/example/ViewController.swift deleted file mode 100644 index 189e19d52..000000000 --- a/Example/example/ViewController.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ViewController.swift -// Example -// -// Created by Patrick Kladek on 16.05.24. -// - -import MapboxCoreNavigation -import MapboxDirections -import MapboxNavigation -import MapLibre - -class ViewController: NavigationViewController { - private let styleURL = Bundle.main.url(forResource: "Terrain", withExtension: "json")! // swiftlint:disable:this force_unwrapping - - override func viewDidLoad() { - super.viewDidLoad() - - self.mapView?.styleURL = self.styleURL - self.mapView?.reloadStyle(nil) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - self.mapView?.styleURL = self.styleURL - - self.mapView?.centerCoordinate = .init(latitude: 48.210033, longitude: 16.363449) - } -} From eb6eeef1c74dd4b524df210b3f9402473e79472f Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 31 May 2024 21:31:30 +0200 Subject: [PATCH 23/44] add button to start navigation --- Example/example/SceneDelegate.swift | 58 +++++++++++++++++++---------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index 6dd5460e7..c6e803def 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -16,6 +16,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var viewController: NavigationViewController! var route: Route! + let startButton = UIButton() let waypoints = [ CLLocation(latitude: 52.032407, longitude: 5.580310), CLLocation(latitude: 52.04, longitude: 5.580310), @@ -27,7 +28,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window = UIWindow(windowScene: windowScene) - // NOTE: You will need your own tile server, this uses a demo style that only shows country borders + // NOTE: You will need your own tile server, MapLibre doesn't provide the server infrastructure + // so this uses a demo style that only shows country borders // this is not useful to evaluate the navigation, please change accordingly self.viewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ())) self.viewController.mapView?.tracksUserCourse = false @@ -39,22 +41,31 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window?.rootViewController = self.viewController self.window?.makeKeyAndVisible() - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { - self.startNavigation(for: Array(self.waypoints[0 ... 1])) - } + let positionCameraRandomlyButton = UIButton() + positionCameraRandomlyButton.translatesAutoresizingMaskIntoConstraints = false + positionCameraRandomlyButton.setImage(UIImage(systemName: "globe"), for: .normal) + positionCameraRandomlyButton.addTarget(self, action: #selector(cameraButtonTapped), for: .touchUpInside) + positionCameraRandomlyButton.backgroundColor = .white + positionCameraRandomlyButton.layer.cornerRadius = 8 + self.viewController.view.addSubview(positionCameraRandomlyButton) + NSLayoutConstraint.activate([ + positionCameraRandomlyButton.trailingAnchor.constraint(equalTo: self.viewController.view.layoutMarginsGuide.trailingAnchor), + positionCameraRandomlyButton.centerYAnchor.constraint(equalTo: self.viewController.view.centerYAnchor), + positionCameraRandomlyButton.widthAnchor.constraint(equalTo: positionCameraRandomlyButton.heightAnchor), + positionCameraRandomlyButton.widthAnchor.constraint(equalToConstant: 44) + ]) - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(UIImage(systemName: "globe"), for: .normal) - button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) - button.backgroundColor = .white - button.layer.cornerRadius = 8 - self.viewController.view.addSubview(button) + self.startButton.translatesAutoresizingMaskIntoConstraints = false + self.startButton.setImage(UIImage(systemName: "play.fill"), for: .normal) + self.startButton.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside) + self.startButton.backgroundColor = .white + self.startButton.layer.cornerRadius = 8 + self.viewController.view.addSubview(self.startButton) NSLayoutConstraint.activate([ - button.trailingAnchor.constraint(equalTo: self.viewController.view.layoutMarginsGuide.trailingAnchor), - button.centerYAnchor.constraint(equalTo: self.viewController.view.centerYAnchor), - button.widthAnchor.constraint(equalTo: button.heightAnchor), - button.widthAnchor.constraint(equalToConstant: 44) + self.startButton.trailingAnchor.constraint(equalTo: self.viewController.view.layoutMarginsGuide.trailingAnchor), + positionCameraRandomlyButton.bottomAnchor.constraint(equalTo: self.startButton.topAnchor, constant: -12), + self.startButton.widthAnchor.constraint(equalTo: self.startButton.heightAnchor), + self.startButton.widthAnchor.constraint(equalToConstant: 44) ]) } } @@ -62,10 +73,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate: NavigationViewControllerDelegate { func navigationViewControllerDidFinish(_ navigationViewController: NavigationViewController) { navigationViewController.endNavigation() - - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { - navigationViewController.startNavigation(with: self.route, locationManager: SimulatedLocationManager(route: self.route)) - } } } @@ -91,7 +98,7 @@ private extension SceneDelegate { } @objc - func buttonTapped() { + func cameraButtonTapped() { guard let waypoint = self.waypoints.randomElement() else { return } func randomCLLocationDistance(min: CLLocationDistance, max: CLLocationDistance) -> CLLocationDistance { @@ -105,4 +112,15 @@ private extension SceneDelegate { pitch: 0, heading: 0) } + + @objc + func startButtonTapped() { + if self.viewController.route == nil { + self.startNavigation(for: Array(self.waypoints[0 ... 1])) + self.startButton.setImage(UIImage(systemName: "stop.fill"), for: .normal) + } else { + self.viewController.endNavigation() + self.startButton.setImage(UIImage(systemName: "play.fill"), for: .normal) + } + } } From ab6f05c43a900ab1b71b9f6f74495dd7f50568f3 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 31 May 2024 22:32:58 +0200 Subject: [PATCH 24/44] fix tests --- .../Tests/NavigationViewControllerTests.swift | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift index a9d1e9ec5..c33d72dab 100644 --- a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift @@ -16,12 +16,12 @@ class NavigationViewControllerTests: XCTestCase { lazy var dependencies: (navigationViewController: NavigationViewController, startLocation: CLLocation, poi: [CLLocation], endLocation: CLLocation, voice: RouteVoiceController) = { let voice = FakeVoiceController() - let nav = NavigationViewController(for: initialRoute, - dayStyle: DayStyle(demoStyle: ()), + let nav = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), directions: Directions(accessToken: "garbage", host: nil), voiceController: voice) nav.delegate = self + nav.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = nav.routeController! let firstCoord = routeController.routeProgress.currentLegProgress.nearbyCoordinates.first! @@ -89,10 +89,10 @@ class NavigationViewControllerTests: XCTestCase { } func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceMoreThanOnceWithOneStyle() { - let navigationViewController = NavigationViewController(for: initialRoute, - dayStyle: DayStyle(demoStyle: ()), + let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = navigationViewController.routeController! navigationViewController.styleManager.delegate = self @@ -108,7 +108,8 @@ class NavigationViewControllerTests: XCTestCase { // If tunnel flags are enabled and we need to switch styles, we should not force refresh the map style because we have only 1 style. func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceWhenOnlyOneStyle() { - let navigationViewController = NavigationViewController(for: initialRoute, dayStyle: DayStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = navigationViewController.routeController! navigationViewController.styleManager.delegate = self @@ -123,7 +124,8 @@ class NavigationViewControllerTests: XCTestCase { } func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceMoreThanOnceWithTwoStyles() { - let navigationViewController = NavigationViewController(for: initialRoute, dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + let navigationViewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ()), directions: fakeDirections, voiceController: FakeVoiceController()) + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) let routeController = navigationViewController.routeController! navigationViewController.styleManager.delegate = self @@ -161,7 +163,7 @@ class NavigationViewControllerTests: XCTestCase { // We break the communication between CLLocation and MBRouteController // Intent: Prevent the routecontroller from being fed real location updates - navigationViewController.routeController.locationManager.delegate = nil + navigationViewController.routeController?.locationManager.delegate = nil let routeController = navigationViewController.routeController! @@ -181,7 +183,8 @@ class NavigationViewControllerTests: XCTestCase { // wait for the style to load -- routes won't show without it. wait(for: [styleLoaded], timeout: 5) - navigationViewController.route = self.initialRoute +// navigationViewController.route = self.initialRoute + navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) runUntil { !(navigationViewController.mapView!.annotations?.isEmpty ?? true) @@ -257,7 +260,7 @@ class NavigationViewControllerTestable: NavigationViewController { dayStyle: Style, styleLoaded: XCTestExpectation) { self.styleLoadedExpectation = styleLoaded - super.init(for: route, dayStyle: dayStyle, directions: Directions(accessToken: "abc", host: ""), voiceController: FakeVoiceController()) + super.init(dayStyle: dayStyle, directions: Directions(accessToken: "abc", host: ""), voiceController: FakeVoiceController()) } @objc(initWithRoute:dayStyle:nightStyle:directions:routeController:locationManager:voiceController:) @@ -273,6 +276,10 @@ class NavigationViewControllerTestable: NavigationViewController { required init?(coder aDecoder: NSCoder) { fatalError("This initalizer is not supported in this testing subclass.") } + + @objc(initWithStyleURL:directions:styles:voiceController:) required init(dayStyle: Style, nightStyle: Style? = nil, directions: Directions = Directions.shared, voiceController: RouteVoiceController = RouteVoiceController()) { + fatalError("init(dayStyle:nightStyle:directions:voiceController:) has not been implemented") + } } extension DayStyle { From ce59d6fc6e6652cf3c50cd09c98cfe25c08486a8 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 6 Jun 2024 13:45:47 +0200 Subject: [PATCH 25/44] make routeMapViewController non optional --- .../maplibre-navigation-ios.xcscheme | 2 +- .../NavigationViewController.swift | 94 ++++++++++--------- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme index 8a565c94d..8e6dd8726 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/maplibre-navigation-ios.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 6 Jun 2024 15:10:00 +0200 Subject: [PATCH 26/44] fix example --- Example/example/SceneDelegate.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index c6e803def..2b6d856bf 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -32,10 +32,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // so this uses a demo style that only shows country borders // this is not useful to evaluate the navigation, please change accordingly self.viewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ())) - self.viewController.mapView?.tracksUserCourse = false - self.viewController.mapView?.showsUserLocation = true - self.viewController.mapView?.zoomLevel = 12 - self.viewController.mapView?.centerCoordinate = self.waypoints[0].coordinate + self.viewController.mapView.tracksUserCourse = false + self.viewController.mapView.showsUserLocation = true + self.viewController.mapView.zoomLevel = 12 + self.viewController.mapView.centerCoordinate = self.waypoints[0].coordinate self.viewController.delegate = self self.window?.rootViewController = self.viewController @@ -107,10 +107,10 @@ private extension SceneDelegate { let distance = randomCLLocationDistance(min: 10, max: 100_000) - self.viewController.mapView?.camera = .init(lookingAtCenter: waypoint.coordinate, - acrossDistance: distance, - pitch: 0, - heading: 0) + self.viewController.mapView.camera = .init(lookingAtCenter: waypoint.coordinate, + acrossDistance: distance, + pitch: 0, + heading: 0) } @objc From 8efe7949840e7aa51998032f21e2e57ba6ef6c03 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 6 Jun 2024 15:51:45 +0200 Subject: [PATCH 27/44] fix idleTimer disable & statusView for simulatedLocationManager speedMultiplier --- .../NavigationViewController.swift | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 91a62b240..fbb12e4bb 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -456,29 +456,6 @@ open class NavigationViewController: UIViewController { self.view.clipsToBounds = true } - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if self.shouldManageApplicationIdleTimer { - UIApplication.shared.isIdleTimerDisabled = true - } - - if let simulatedLocationManager = self.routeController?.locationManager as? SimulatedLocationManager { - let localized = String.Localized.simulationStatus(speed: Int(simulatedLocationManager.speedMultiplier)) - self.mapViewController.statusView.show(localized, showSpinner: false, interactive: true) - } - } - - override open func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if self.shouldManageApplicationIdleTimer { - UIApplication.shared.isIdleTimerDisabled = false - } - - self.routeController?.suspendLocationUpdates() - } - // MARK: - NavigationViewController public func startNavigation(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) { @@ -496,6 +473,15 @@ open class NavigationViewController: UIViewController { if !(route.routeOptions is NavigationRouteOptions) { print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") } + + if self.shouldManageApplicationIdleTimer { + UIApplication.shared.isIdleTimerDisabled = true + } + + if let simulatedLocationManager = self.routeController?.locationManager as? SimulatedLocationManager { + let localized = String.Localized.simulationStatus(speed: Int(simulatedLocationManager.speedMultiplier)) + self.mapViewController.statusView.show(localized, showSpinner: false, interactive: true) + } } public func endNavigation(animated: Bool = true) { @@ -515,6 +501,10 @@ open class NavigationViewController: UIViewController { let camera = self.mapView.camera camera.pitch = 0 self.mapView.setCamera(camera, animated: false) + + if self.shouldManageApplicationIdleTimer { + UIApplication.shared.isIdleTimerDisabled = false + } } #if canImport(CarPlay) From e064c969160bf8fcac9acf0feb391f5785170b7d Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 6 Jun 2024 16:14:02 +0200 Subject: [PATCH 28/44] add backwards compatibility --- MapboxNavigation/NavigationViewController.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index fbb12e4bb..68f5436bc 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -200,7 +200,7 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { @objcMembers @objc(MBNavigationViewController) open class NavigationViewController: UIViewController { - private var locationManager: NavigationLocationManager! + private var locationManager: NavigationLocationManager var mapViewController: RouteMapViewController var styleManager: StyleManager! @@ -329,6 +329,7 @@ open class NavigationViewController: UIViewController { public required init?(coder aDecoder: NSCoder) { self.directions = .shared self.mapViewController = RouteMapViewController(routeController: self.routeController) + self.locationManager = NavigationLocationManager() super.init(coder: aDecoder) self.mapView.delegate = self } @@ -355,7 +356,7 @@ open class NavigationViewController: UIViewController { directions: Directions = Directions.shared, styles: [Style]? = [DayStyle(), NightStyle()], routeController: RouteController? = nil, - locationManager: NavigationLocationManager? = nil, + locationManager: NavigationLocationManager = NavigationLocationManager(), voiceController: RouteVoiceController = RouteVoiceController()) { let styles = styles ?? [] assert(styles.count <= 2, "Having more than two styles is undefined.") @@ -363,6 +364,7 @@ open class NavigationViewController: UIViewController { let nightStyle = styles.count > 1 ? styles[1] : NightStyle(mapStyleURL: dayStyle.mapStyleURL) self.init(dayStyle: dayStyle, nightStyle: nightStyle, directions: directions, voiceController: voiceController) + self.startNavigation(with: route, locationManager: locationManager) } /// Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. @@ -414,6 +416,7 @@ open class NavigationViewController: UIViewController { self.directions = directions self.voiceController = voiceController self.mapViewController = RouteMapViewController(routeController: self.routeController) + self.locationManager = NavigationLocationManager() super.init(nibName: nil, bundle: nil) @@ -458,7 +461,7 @@ open class NavigationViewController: UIViewController { // MARK: - NavigationViewController - public func startNavigation(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) { + public func startNavigation(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager = NavigationLocationManager()) { self.locationManager = locationManager self.route = route From 04e4fd49c5c3fffe87e3053812b9ae5bfe806eb2 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 6 Jun 2024 17:12:44 +0200 Subject: [PATCH 29/44] fix tests --- .../Tests/NavigationViewControllerTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift index c33d72dab..6a342694f 100644 --- a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift @@ -82,7 +82,7 @@ class NavigationViewControllerTests: XCTestCase { routeController.locationManager(routeController.locationManager, didUpdateLocations: [taylorStreetLocation]) - let wayNameView = (navigationViewController.mapViewController?.navigationView.wayNameView)! + let wayNameView = navigationViewController.mapViewController.navigationView.wayNameView let currentRoadName = wayNameView.text XCTAssertEqual(currentRoadName, roadName, "Expected: \(roadName); Actual: \(String(describing: currentRoadName))") XCTAssertFalse(wayNameView.isHidden, "WayNameView should be visible.") @@ -152,7 +152,7 @@ class NavigationViewControllerTests: XCTestCase { routeController.locationManager(routeController.locationManager, didUpdateLocations: [turkStreetLocation]) - let wayNameView = (navigationViewController.mapViewController?.navigationView.wayNameView)! + let wayNameView = navigationViewController.mapViewController.navigationView.wayNameView let currentRoadName = wayNameView.text XCTAssertEqual(currentRoadName, roadName, "Expected: \(roadName); Actual: \(String(describing: currentRoadName))") XCTAssertTrue(wayNameView.isHidden, "WayNameView should be hidden.") @@ -170,7 +170,7 @@ class NavigationViewControllerTests: XCTestCase { // Identify a location without a custom road name. let fultonStreetLocation = self.dependencies.poi[2] - navigationViewController.mapViewController!.labelRoadNameCompletionHandler = { defaultRaodNameAssigned in + navigationViewController.mapViewController.labelRoadNameCompletionHandler = { defaultRaodNameAssigned in XCTAssertTrue(defaultRaodNameAssigned, "label road name was not successfully set") } @@ -187,10 +187,10 @@ class NavigationViewControllerTests: XCTestCase { navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) runUntil { - !(navigationViewController.mapView!.annotations?.isEmpty ?? true) + !(navigationViewController.mapView.annotations?.isEmpty ?? true) } - guard let annotations = navigationViewController.mapView?.annotations else { return XCTFail("Annotations not found.") } + guard let annotations = navigationViewController.mapView.annotations else { return XCTFail("Annotations not found.") } let firstDestination = self.initialRoute.routeOptions.waypoints.last!.coordinate let destinations = annotations.filter(self.annotationFilter(matching: firstDestination)) @@ -199,7 +199,7 @@ class NavigationViewControllerTests: XCTestCase { // lets set the second route navigationViewController.route = self.newRoute - guard let newAnnotations = navigationViewController.mapView?.annotations else { return XCTFail("New annotations not found.") } + guard let newAnnotations = navigationViewController.mapView.annotations else { return XCTFail("New annotations not found.") } let secondDestination = self.newRoute.routeOptions.waypoints.last!.coordinate // do we have a destination on the second route? From 5c845cd1a2a46590a5053b75713427495a447d65 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Fri, 7 Jun 2024 17:11:13 +0200 Subject: [PATCH 30/44] address PR comments --- .github/navigation.png | Bin 0 -> 233221 bytes CHANGELOG.md | 1 + Example/example/SceneDelegate.swift | 3 +- .../NavigationViewController.swift | 9 +- MapboxNavigation/RouteMapViewController.swift | 101 ++++++++++----- .../Tests/NavigationViewControllerTests.swift | 2 +- README.md | 118 ++---------------- 7 files changed, 89 insertions(+), 145 deletions(-) create mode 100644 .github/navigation.png diff --git a/.github/navigation.png b/.github/navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..c85f7efe79735214e562a55c76c1264fb7a4e4cb GIT binary patch literal 233221 zcmZ^K1yo$kvM%nfgS)#E+})kv8rv^7)f26vB#n%Kj{pV+hAb;1p#}y9`N>1d!@+#!Yzx>Nfq_AxSc{9R*osSw zJK8zAs5_aMSx8$rSh!f5sY#20f$_%0X&P7&YGMgxwKXzOPKM5xmL{XTbEP)fsm+A_ zIeNEfv|nRPv1<}d96epnv8LwOeJ!@86T%Wj>nq<%+4|7yqam}YI*Vm0Y*HzN3Z;FV zewHqn#f3j<<7n(iX|?SdUwb;pz6dzna6R8ZOl)P;OWUA=V8pLs+MnBL96lHr;B)Kq zpGNkdW@3o{PXC=6oM0E!^XKe>bZBGg{@26Dx04zbY4E7^@a1aWG4dS?uy^di27@3d z(U%eUCDTXv0e-t|(jAQcW-x6)F6w2&R7Mhel#wJOZ6)x1U^NQ%kmCL;6s+szYPW&*86mNm$dCjv^>qdHMgi*X6wd5 zv=56mYxpY)FoTZHa2JFtp#d{Z-%i3Kw~9)hnO=sn`HXL9o1tFreF9@c=Mt~YY)07# z?g47qNC&9IU|T!%Om#jF6vTCk*7Q;1aoSeg!`D%dtp_-k5dP;(->mtTroWU}#)vYW z$L?OEYn2HNXjkN>i@g~!{lXfe5{xj+Msgcrb$`8k_R!ABfZIv_f{Ta`q+-!(z7iV= z)NZCJMl))03ryHq8&ObaWkw5b}E>j;M zqr;^-qeGsrNbj%#kbx(wTEDnXI486a{M#M%2BdEUmCS`om^AH78jA{bxwM`kNXEhZ z+q^p9zQO3d?mdhOC)m1Fi{Hud- zD_l<Q5At@gp{eA#(HjxU1eJ7Or_PystF;4_{lppl($|PQO@oWX!dGqnVft=q2Yt8?F{Gjqe95h=T z|5OlO7CN$)ii%+LpFA8GG&lwr9u3}But4+>1ci|>=Qw{SBd^|H5faOLw7B>$HL-zWc1 zHVZlFzeL<@1<7?3RY=7hT`WksnOT`x$%PO|Nl67<%q{uUB&7ZW|NJIMZsq3Y#K*$o z>FLSr$-(UC@{NV<%a<=Ktn4i8>`b2$Os?JzZYEw#4z3jcuH^sLBVplc=3?#SX6@)e z`cJ(krjG7zg5>1?H1uEBzx(NCZTa6VIk^63Sf2xA`KN@1jhU6@e}h?gS^s}v|CIb2 z_OEgMyE%b>D&tdjv9|c^@IT!WViWjR1OHFne|JydpIZ1-ti3GkbR?|pEgW3`!LW01 z@d~i~uSNeWRQG>CxwyFhC-i?5{Rb+*^3P2DkD2@TvivLeb6tcG1X%uSjfD`LZ{F;{ zz(l}gB}6s6z)$^Q{qzT$-+%cxcYM8L)^oW)4(-#JQ$!6DDTm)HNfd*FCu13vzc!?Y zr(+S^+gFw$gQp6^AuR~Aghgs$b7+-q@iF+>)b-xOQN=JW)?lI667%g=Z++eOQ0WWD z!{pNe=bVobA94;S^gk3&4m~gb?LxTMJmfFjLf*0eV1_TlC3KP1l{8wMWPKG?Q2afV z1Ah8M&!B}Cc8K0U)qTS-f_ORo8r=nk73?WdF`!h6PFJg55>ciO9q8NhZX<2{Rpn8X1fFeCs&Z8UZMY0JrNv z##*N{@|wJ@n1rZADcxP7mXL9`{4=8=d@9h-pJ36>Vc;i2jOvfM;3ldd2~h007e!&| zPnlx!Ivq1ODYMR{$(BLyrtAJ86b6gwD6lX$zF_?qO3Gd6x?+Pmy%?2@EOBbWNuGIt0bEI9h#dCnmi?1bk%>!r0b9(&gspbQq43#WH^| zSazp5iKY&Uh^OWA-HDPSml^0tJUkt$u7s=RCh<$sdfaWEuovaC(B%K9Xo{W?2rT2^ zzi)_Td3k_UqJ}`!8tHVfMYlZ2jj72Mu(Q1?m?jGDClaCT!Zf%Ngd&DWn}CAg)>LkO zkQjiFuWxRHfmg?^?(FGW4Tty@gpDJcn7DsT>Lp_;7YxgM;c!mT^zi>i#lb z5Da=RFRJ~|fI9ve&@z$lx~pO^g%tRp)=ggDK+ImRLx~Tf?dwu0&OjlV7ZaJr{(gauc*WTJ_?O>z7y~K+=I=(R!>16;TB<^#sSuS>R*ozA#|7^ z_5#49I=ly0gcR7_capUah56*d=`rYhans@Scfzh(U?%q9$m9HYsdrZ=isiKka%o_6 zkE7?0sJ183ny__b+%dmmlW^DHl1!BI*q-&~oX=SZKNCc@LgXsqSB;&c)-=;if^$h%$M&Ryb9YBKiGYnHV!#@vdU%akbmVBwx6EwxrXkk4lF`SuW+_wb8y7-S zSCJT|Lm!mG8tPH~(feks`4dvH8!(xn7r}A3nq*%k;_8JsYZnU^w0*jaI7h5JC&PzM zNn1xgo-7?LDhEynf;2>KwD-X0O6gNxRr$_=XwT$&HJ+YT@1lg>-j2y>3A1WBxauzY zYQ5mF=8=iH0n$7I&Z|>Jvxch0Li{S1q*9$z1S_Bow;Bl??(R z2UZk1+Pr7j%rZLZ9KN*Bf**wF@yDv7xmSmYEH!I$(DUOxe(nv0J&LibcF+XR-OZUU zt)~_mAk4aKtJFpj!sEQSZ0;6KLWK0R_7Nop-HTR4EWqGWTE8hQLdGQ_hZ{A3Jne6a zE34}@O4IOS0Zq<$&NFKl;>z?K2Qn8ECppTOZT7ZlOjNV4Z6q8FkWSv2bt{L`+IFbq zsR+xznbUavr1U$9bm1}f;$_H#>>zMBJ`_@ETYulotbIjlT!_s?fygMOwCM=4&_tPF zwv{GV3$0Nhd(|Rnj{#vDA zo+5y7r=)o`;^Q4yPR(5gRd|&`w-02P-&c-Bl#;M-Xb!h?+5s8+0MALYk;#TW>1c_X zez73Zi9kXNZik8^)xfy_~TmQ&N_&Zi*P3UarwdK#4%dO(b=br42oxwcO8(%6ulj zl~AW0XBp`6>#PQl(~uodSnPGkfLSN|M|p;mK1osWq0j# zPTVHIKG31(GmVK-)SDt9{|g!Cc?(qn!>QQx(uYxl1w#`Lu?ubGXlD+Xi?;5Ip+af3 zmSurxCS^jn9Thn=3Eyhafz@~pB3!#ZQhGhwalyj+87Z7CmSoS|a#p{iyb5}J4R_tn z&zmXEho*K8rB`TgHNb}4D%+@*&K`TWtb!!a!)U^Y+DWaj-m`4G9B$58J4fAxUDD`F z_rX~`5*$&)rcx!sXqVF!2M+@(2B8@(yRpfn{lOy%t`wnGM`FHL6_M)pr~itdzH_cE z_V_y3c%>xHBeqRjc|CXel*Lxd4zIkSxBPci3_dKKYGyY%mF8fB(QvF;Wq(v*bX>(g zDN~wWcAYOCB%`XH&VdCz5HC|kAr5}$pmqXEqBBbOIaL#Z3^(&Um99P9uK%%8(*QYQ;C1P(N29;TA*U^xE1*A`l%N6V-^Sy zon{s2&&(I5PZD5R93VXRx@adKOjR|B?TO)#tU4K;F$Rz;!_}3Y`p2{MjCXJ``L>*) z+30w{wY9*uZ>4-hGDGe8ad!$g&juVf)T6QGPgIt=k{*U>;f+IK6&s+w1<52Qqj1$e z=>fd5Bu%+~Y&Tf+=Vk&ujNnC6a1FKz<=`2A=;cHna&s1}q4(n#4^Em=kY{VP=`M(gim|nbUdq zMfKE#;WfwfpEe<<3UACnu8by~lgJ(V4A(DJ=y00_da5=udVvJOT@7kwDvqISicL?G z!e zP|oSI;=W7E7HvKZZ47V*dzfUP+G*V%)xGTSdlgtJD7O-uoM8KAO)L22#Jm*Ay2Q9q zNry+ksF$=lsy$iq$Rzh}M%>v*%&^lTmA9QT0&-v6@4=!X zEqz@hN7OiO+D_A;pw8SYYv_-eGZmIO<~MA*@ByjBqcJ5VmtW0jY_mH0Wq+7f;37I% z%PMRZ3?c*#R-8H-RW(ngL|fv{xVJs09Q%Q%ln#6}h$orW4Ct4}@`_|s1m3N;P&}Hs zU+d~`blY8#uXOQqXE?LCc4Q&L(P{p&Y3GBZ?7eZ$chK}12ZILaBRtA+W3Ft7j-P{R zZPxmJ8M52iLKCcE*gN0DY63AcoULC}UPw|)$8de=Vr{lgUF=sR53al7vDCUL7RN5$HvJ*j5 zg+n>G&5i!Pg6b!dr`+t91<{W4S&?BR=D6vOt3kmuxYla8d8IzWxdbe5li1*QuywEk z$98-+=EJg2p$;_lj*#iXwI;D$XQ8xb`-r@F#*-i|JsaG+@>2^Ys=G^JnU2jUTyt}^e{Qw`|?ARlSuE+h8CEJ zk43k}CXMNR`S}=z4>>w8`lvusHB?SaHBKKJ=fFo9- zXJFUafU2w+5+BwD3vgWzE~LALO0Ln8g&E6iKHQI8HPA#b8!**_gyF?h2t^{d@G2$s)FbU^W~p}zRoc5(!z9C@s6tVK zzPFjIf5Zj6ug&$n_rJH^&dS&OJ-PT~Hdfskx7vcNei`>~$DsmV6V84p!@Os@{<*Ro-~9U&#eW7AVLa0yPq$*?_;+Gg4wwpI6VtVvM`+|E=N7jN|j6eO}^U*ZF!}5Ep_t z#~=uWz}0#eMyQb2j;Hs>sQ)xueybtXq{Cg79srvqI70ex+zMx_+^DTK+xatoL2J91c89BFl0MN}#(1Jl74b?DrXU ztBn|rL78QT*x0Tb>Bf*{GFVY6rwV`yl(1{_a-q~Sz_snW`9hrQWqOgRqBX=7+_C<) z5cfPEKjhpqY&y4ZU|a4tsyd%&?w6T)UjvV8@cg$D@om_V@m}_q^{L9I zWm3<9W7a|ifsw88)5`H`dxK;>-1p;wd10||*5x`F$htZgs*(s($rnctt?z)Uzp@fT z8&EXsYm8f-oMdp7c`#OZwpDlT5}(hGnYq!wO`HPLZu zz7ZNc>K2zNuAT3V6owNnEXGB2K0Qty81ig8xYqd~{d?_6xsY)?`N5}ifT8s zVh+x!LuS|#zM@xS0Vb|yTb*e=ktQ^7DF|Xn^hj!KzO)#uAS?m+%LrOaK5Dx@nOue+ zM%ffKsq|MNVOgYFc-wNbFCX!8A_)1L3Ub9{0;3_r71YT5i%tNIX4ujDf;?xi!=gb* z`+-XcA%{Ur1Y9-W2*7*$g3jD7%C4A>&3U#PWO_a4lr;o$X80Q7AkZxof&Cqr!}-)rWElU!>1^bTFnNK?T&8BmIsHr6b*d+;CMt zaBPallO^epG z3_g}if?yELJne=g#?3{YR4;s94v1QBN~`KD`uS2d6vQ|c{O2a){a4F95+g0g?tfPY z#sQ#VrVVhyvpfBGvvu8ObQ4w()3UvcwpWm>9S~Gjj=hr+Dabls?us4{jDjxCk9yw@Q;qv>VVZ^(FWY6P2hu>r zrhK$qbnhPOg`^MCDL?_m5~{XbNs2?a*>TAq&;5(Eq6(-~1ce?Jg=}tY3RifS=4^)I zr<)pzy$gwfG zu)pMOjg9Jpu~Op}ljDsQb2BBq#%?UMv?|<4?(Xh74d(E~UO~1H_5k3OKs(cXa~#b- zukN{0C$zsbDdCx)`!+irIBIl+lot%OzThd|`OZJ|8A?gFMYp$$(oP5Jk5OyR1epIC zySqG&`|-=1SS+~9xDO10KNIrtuW-xui}qg8@GW5oX9Kon3&*KCDH#(IPuF!+Kp^D` zq@xxDO{yQ7=PPrJ^#XLOk2lpqa`}hyC={1d>-Z#nc<^Cx%K{VyUd5o1QB`Ohdh(PZ zTKIZ@&TTFeeXSQ&0|&EMn=iLmw1b9Dl4{hZ$AZs37M0l=5DxsQ?WQV_HUftPQ1m z9V)w4^~b83snVddDMxAtqBl&bXj~!VOHH2_O{tf1uQPOdRm*W;CIlp%G3)m%9><|h z?+mVj%1VR|&kHTxmmXi3jjF%|V(90D*kvZbJw+C`J!q}Xl}iWERe*=fWt-2+9P#&v zM@@j>%iom}4_~H@z-J7%xlmPSUFfe__*ipAp3(8|KM3vOhIUBlP3?cjjocq>1#54J zEi};|FRL#ZA|d@SXSBc7*H}Ua<+hLt0FkGONpG@`Kvc1fNf(N9OMw)b;#niKkz3r# zqRO6hT8>j?5b;D8{Pl8d9hP4y%SMe6#~E@Hi@n$$zZVX`1RpR6d;?kfNckjJN^1XZ z-hnzkS!Dz!o*GFu=SG?Sfd=Xv>B=mhcOki9q^!~G=KQJf+WF1Rn03%saU+RW8E@yy z;CVZkMk)u>6S)sn0m3@0@xmD&VLhrWx_G9kM^7H{)W~1Z+?&{EYe6bZJclwd+5kL^9wda}Tc1i|lShG1XVEX)Xt z5};Sw{vHuTpCe44olB7sE4G*7PY8&0i!)1rFf}5*Kmv!3#bHv*1&)o6f0v8Bs=@R9 zxyW>^3~rW9PftHRJ)JK=_0y<_7~+25dz-G|GM_a9QvRpzgM=##v;;^a;&nN(0;lvn zzi0`5Oc*BihLbb#(ecG0B1FwJ0<~qmOVq^q_D{b*_L02DF#ZCd$F?g);9=E~PIN8L z4UJ6Ck)du>T4aZ&IcOYE;%!ZY{W^i1pZb;3Hrbc?RSwH?uOM>>+RabGGRJ3hyBLCy zTMp*6$@5bEx3#t{4|1p(5{SBQv>m+&my{sR|E4iiLmeTXHeG~Vw}1VVc>zZiYAguS zES~L|eHY+$q~xx8K3vyWgm+M_jb^bHocjYT3@$ic)7uJTNUIoiN0LrGUUg_%oJyns zB$!S_E2Z6(Td8$+Dal9sU52hBj!~X&XPbUVUS>{ar4ZOf1p@-@3@-2Z;xq>@S%q#} zW})gYP5%V^{xpMkQYi@&8g9M4{Ou7Klu9n>V26Ws3Os#)mXPl2W87d+(u}TUb;mQkW?MNV;Zz3gBZ+ zJ7dMed>_^&YM_;=D~}BDO@M&kPpM#-1i%+U)`)^0@fGk`z!6QWNOvEPU6rexyZ+AQ zm7-r6=99*X1A9g9lPr`+Cv!gL4)!QmsZj4yIqmKSRkeholhWOWGMj;50&!}@+MA--i#Wgw%jIC89zAH5L* z(919mZI|t$qktYcotKYg)bK{2IlAY(M!<;2=X{vICfsFKxi;qTUV*c2uCI0UBO%^3 zwOsLqaCCMU*3Bha7q$7To@qiRNUI_4*?_F&i%G#hz9R=OMIRqud~`7tnbHD>@jxaH zy%6U7pe&H8TfV_Xafnsz{TnE#NERaaHSN>+IGq3iqtbeq>x(7x`EsPihJjStXr%S# z4pi?=M2eOW9lFZ+mKzK_DQlw!>(-xts^RUox1(y6C-7X zk|n;IemtO-5mlD6T`3!qE3r#nbar8<=kdaB==+REhoP4x*db#K4WJQeQMlY7rlLPn zd|FG@(2eLOlosO9*2aAp&MTt5M}@H;U}vZsYb&yJ>S4?Zm`IuCg>26F91NXaX1=(| z1+3kEnizU`brbeAA_Zo66O*1+QLofuL}lA7wUjCuj(sb`uv^MK1M4p+5Eu-24&Fk0 zJ*@W-=)iAUBEat+u)cAAk|iSwH{VS5BqNgAJdLX!-X{ghd;`aMu0LJ@?&dYox1z^s z*Y}#9V@)!rNbE}zf2bdd%WF8JbHB3_rw#mgQc02R5=OZ@@9$AQV%jKhu{xu4+x1{V6}*!&KT~Np3xZvl zswPAR!%v8$#cm~JrSThsb-zSb|B%d`VA)Xg#+~t2rm;Tpq-t(Ll7d;%J*HA2X()AU zZ45p`Q5nv?Cu)(x52TdMEQ=_=mkn8uh}huoLW>r|1{hC=KnD_%_IKH_^yE0vi$UzS zM&Rhhb+5`gLBd90txcZSqEkuh zA|=&twDDa0zIDwt1c)(aI=o#d?|WK4_oIV_)1w-Q*9A^}KIW^3MKn5a>Hhr3pae2| zd+78>%@KJs$X4$DFXaLj#GINS^XzO7u+o>QynVbroC;cnB)!CKj}! zzwKM?-Aa7F4TV%8(0D~7Wo*c{ifGoGXAi3~Y*aF6xNfp(tR=I}^WnUSOQvEN5sJ+b zQp;Kb)xbn4ak@Ag_yHs#GxZ&Fz5ww)1Y9Ztyk67!*^l!#|3d?Pqn-I1{FwKb4&UM$ z+0{YgHC%hp^SSU=>)_IKiLFp9iJuZsqf5Gxu$j8Ne&Yi`G|TH`6BVYoaYH@RccKX6 zYX6#6l&aiNc~|lA#P(+JhXECSuIpi=DxO=YF?p}^IOVzIBaR1Qgz&o;>(kQ{0`16S zOqQNm=dfO2giY;)>aMrO#cRM+h4WOez|hkh(LOQV)YjE~`h#&To(0LNzcl>^R5GnO zb!ez)U)YQyYi}xg>hbyj1qWypBjH&9t&8Zu^inBd|pHiXvKE9pMuJ7^TJ#+r2U|HLNFwr(Zer`}lm^UBK(k)7j%exHnLz zGrS)C7_Y#KhhuuUn=Xj{f}C-rn^+Gru>bu0SGlDdJ#~k&7=>o&F*ytEuY8Zj$V^e@ zN(w60Utai0cNliXBm7xYKz@cn9RxBMHcZ?CS$wicYge@VB}(Uvfp3u*P;3oE4r-*i zy}eSFLZTG0=aXo!Iil=)*QYLJ=@C9{;ST19?N-&crCk!#GMV)ztQvyXvn`i~+c(D^ zO$9}x%5`LJ7#Gq4bbcBKcZpx*L_b})-Qzo^7H^W+ZRJ0{j(7qk+z(#FyMbyLwbJN5 zSz|@Td$$c)E3d#~?NW-O`SSj1ts$um>xewBtK}lVbKlV#e?ao43?v>{TaU2WbRb*$ z4{&??3@%aq@Ubo!SMq(ljS%3)+c?`XL9BEUDi;^m4?cH#d#~)6T+^!QJT0^Ovv$S) zqM!-M=;rC)&E1yT$dzRp7aX3E+GEQT3mX3k$gn2b$c6w2h?z+td3-A#RhemtF$m zzLEF>pa)AHuTcYnvtJ3>SwU#04F0@3MFOV?VPu6dqm}9NM9j(_Ftj5edZZ+!-Astj z$fmd`)~v54m5EH$w2Mv39~kmQa0XRHH?W@1!az8r7z z7UB-^pAjEI!FesGpBNymVbq12CvTMr36i~yDyl_0o$fTn1C z1fE}j-gU}DSJ$C&=~w7d`Q0_dn;(4-SJ&e#yMJtJ2hV#i&1n~S|4FVRIICs>wjn>kY6)QKQsXAgI za5fD4NtLNAZtv9<{7)68>&u9Jpnh#Sg?|-r_;_AFva%oY7jDbi+MZg=`8l{(p?Uj; za%$|<(9!M0VEA;X?(gn*b)IKgIsq{74~Mx{3IDsk-;A55eiqN+lqT2J^!Eh#&R#=@ zW>P00`{8pSO4zf5CD64RMpx1{^3ZBELhk_yoIOQV;~GJzU5&13CF;>rq_7ACFM-hL zXe?8iGa3BoUI-R8G8vjbK7*sYu5a#!qH5D(G^zt{?j}ktv`{S=L5LiTNx@AaOIVo! zj7dx0Ll7FVk`VCancRjDf0s7Qy-;%TsJroXt@LJ~mr6zJz5?+-&L!jy>s5K4|7P7< z9a^#>&4UfNhfopOGV+ ze1=WmbEljQ4Z&cU^d<>i?CON9eKSLx^qLYY0r6(?KSEIcrmyMW7(CnZ;fZj0@Yp8# zc!V9OicJb5JIXUxOnk_d_}TTi(`6g-_0ktsyL(&!law?m(gJoC40ng=Acle67PRiU z8Fz4CIdU}>_}<8VkNqnpnUZ(q=dX>JvfMu%)UU6H!WWyVY)=N16tfTC^8B@$S@28h zla(+dAlO@yp$6o9&f5;1cH?*|kAOCi!HEfW zNcsU!`;W7@Jz*uoESf7X>0Flq-uG2iRrRKW2m&9kH^(dKo}+!#1!@#~a5NkNs2&(ju&9IP+H0+fO(0G#sQ^5^|K;d&Li`GMW`MBK#Lw0q97 zUUU9(HdnhdfIZ&I0GFy*IpityAuI6s5fD7Jc}E08VGE*?|5onzaaUwSS0NcfUQy2= zc|?V7gs_9rTvU(k*qQ8IzH{}%M-{-5vXTAFI!T8X7xi0~_`U1dnlTHj_m2T_m>(0b zwH#K~vFUA*qokw|1#MP`>JBfuryypbzZgypKTSlwwIF0+v)SP67heAZ{nvwi>enUT z%4xFza0{x1kzd(EAs?P6S3Z)ai1ZQYpPdYS+Zj`Hs1dV(N17Chu`QH&vrdJ8oL-1@k z6X!n7WgS_amlo5&I?Lfp&GkNs^qx9B`!=0({qR~5O*H@!IJAPt;<5~V6MoTcT|e*o zz4gilD7f%vX`~*B|A-WR-Bu8qgbY=EJTdx+VW{oI_;fxJ69+t+cnJ`ffG#tp{G`4= z99Pwvt~6L2cLaqh`yZ=6?z03|{6Gaq<*+nggi0chniq5Hk86M6wzBi*cxI;8gi%4m zX2_w&fi@?`7ZC*BM6!Yjgq-=iot7T1E&{vC zt?C~n8Ea6~>BlZB4-G*ifmal-OQ?Y*K|$t(bcN*7iY3EQq~Aj-Dquk1)!Das2!B>3ClWdliqyimfksMNZFb~?-y_!ROUCckhgIbwYfsn!w_$PK?P^_G z+8g@S2q9;-vhGfTS0d3=rODy*pG*jp8VYp9^lC5?Bx3`K;Zk;##E&aIP6ycXD*72! zr*zk`jndeZgmOkh*Mfzyj9Vfek4ApZTbUxGDE6aa=!xqRx<}#B!Vhb%n&~ji6NMp^ zqx*k4o^BWQ94ofWNS>!7v1C}m#w^keN`pdDA3ir<2F737Nxl=twZM-zxty&clDG0d zf*R2A+n?bqCPhU>S~@yWdHL840sMY54_d{Phw_P;kPwIxt;OZ}@*q$_D%YO}fI=4( zG^Yp%o+dlU(QlUkVUOSp(-%S)alZ{GI*9cJ0_VK*M(K2|4f9&T$ZI>CmWgQ{m)5B{ z`A)RAarxA4tyQ&)-<(3k%B%b}d5xx0e)DP4bAD(jl}=rQh`fvNk(vG3i8EoBm^SL2 zRkOosXiVN|`=v&GXs9r9T{v1#nIM$PUCm%?)zc4SBU?bccohEkMCDd`9b11v0uly4h7WVFGEX%N~vVhq4(wraM z-@22g+O$GDDW6~SI~paAhyjPH-cJc>RENw(5f_yFM&1s}LGnnGu)m}exGrcb zw~y(^t;=5vpxanm0FH(mPSAPeE1jb5C`~XU=F`2x=pWGCrfG;Rm3Pcw7)TpD(v~1# zGzQ7&+wYSQ{@nMDLx)qy(Tj2XcN0kKv%i5o!^fl>b|Ngcb9xP%DQ;c@e%5|vKLkb?<#lt?L^1V*TYu|Od^ z8vu@MYSneVpJP{qyU-|~irl{u7vEp;ce!H;?IV^xX_-<6zZKJgwa?`pGw(6XG+F4e z57l%kcOuhRL-aPC#P^o1 zRnWoWmiB1~wBy+HATperyTMg}%Sc4q(=GJ=d7P>dFek@8^nzwMTD0NpU9-26%VnFU z28)3$vOE;Mn9%sb{0ogt#ny-#P_uAFi+eT=Hf^3435Ba{)6)Hozk=$1F|uc;iF)r7*X z890Rj(OKn)og)g3PN|!!ri=L(6`il8N{>H=&rE#Pl#ZVt^pJ|T*z%$$s5Z+0WtR4i$8xcQEjwU=2M?pUR{1) zrdcpf7j{#Q8WZ#Jv0unllm#Zc45W!B?lHWb!7`}2cj z`jJ%Dcs6{E)FV}|XL=UEwjsdwC`e^`K}4QwS2aV`1|2vFg7-g?X`|X_e)DVsH$UDV z{GWgg&q}uesuxh#xkK&+IezIkurGGwU2hjjo?)NfmD;&70af_RB-bGTvfWhDyC8TF zgsg-Z&5@`iM$bLCNyg#c2cF8n*!o+}4*C@@5WrsilReH_hfMOC|o^p47zlF4m5tCFOwAbHf^mMvbOeY~F2Qm{XQfM$s^Cptf#hPPjOxY+8!2K@tuKylPcfv z78aoO@!lwE9|atx(9~(1=@%`9S-oH)Wv&$EXXG}?&6$^%`DH@h{eJ6xG}AaJEVRS* z)`dVJx}ZlWF`{UR-QXsvIEZaSx-kCTb>xSUT7q*xovX__ujAfHpi(z#8j@x&&xThK zDy4>|_Vy5g)v(-D}&ke~$B%hlp~LIFC$#NU(;+0CBIvhzPcT1}C1l2*>*{VXU zqHa}rGsqH*r?II{^R;eo286$hoHWAP>xcVXvSgJ9aCsE69*{*z0L{#=3m2!^`bw!? zDPMC4r9Z;_bi0NEW>IsfnW5E2%8A2IPw@Kkkq~cKw*5Sc`j+CqG#;0u%!N3C5|Zsg zB4p4icp-~2R!!VbMKIrBrY>tFZWa+v9j>=3u`l{_iShTOj;HaErt`l!hRSA{Ow*cx};i9A4lHSGHmQ$JP+z}c7n;he?7 zaBmGPKPQEfqGx9EZJmFaD=8>um{L~O!3hqN-r2~Ku~Nb#H#Q64>-hlFVU$Ne3~jy! zQ~DlT&kC1pj;rcHL=_!eNXZs~r3o>!inDGai;6yULtH{O zqRFJLr5wrZF;A4CAIwkS-UKPdbAtOf;DTbNx8Pvf_%PDfq1*$36<116KQ{&;8L$^y z;H0WLBlOGfb(ciD1)k(-L^=W)1K1LckW5)2-#b`9x%-8qI9IJSl_f~=lwp9Xp0a3C zykghsB7;@LgjH+=#7r;Eg#44fC1kJdX@>*t@&b9sypg}|e6x0^v+euieeK2Y%f~Lh zzz~=jO{aR_Bf|nZ!)FxcUq}+S$OJ#xv+wuI;D)HNmkXiw!u^w32Vnoz5;O` z$ZYpn@uUo3m4tk$2BVZ^4j9NLtpi}iG>EIi9x-%jQ?2jA)%`*lm!;_Nym@ip?^ZBo zHRe3O#lfIFV7*$Ni{&v3SdOh+Ypk!nMdp z8hybvdK_$)i<8kvLt4lq8>f<~Zwv_ZMf*O-!E(9?ljq`C**o6hJwq0&hASgg?`dT_ zvHT$e#%p97bat6 zIZ8Bf;}KFU_%+v(e&{esAYrl@oRk>zJQrlq447=7q!uFKr z4`to&^V5lb+*0TGax<#_&m$La^#lQkPfBXCKr1TmPG%`76mo!V_<||LoboSc0Y@6v z#G>*=Ct)Lh9%}crg{Y6>*-JqYc=QCFIWEmDMj2XkDoyC?6yU^s^}$1c zF4WhUShKIqUt)*NlSaRi8;6w@V43JxCXgjB7Ro*h>^475p5zERJhv z!PizRGKoskC*k}01e2LUN&2bHkKFo%r`|Xc6&pyk7S&qw#0#}09c7r`NEK16-pR$n ze2SmDca9OJE%_O6S+LO#GJ&6(?7Oc9ojh1Sf)kvQqpw_9DCRE6Y?lrC1}`j^MOlfJFl;48h#yyGZsQ-}S@RPytL=>!}My>&4F4{Go%(BAlD2YRRSq$~lH2!XLi z%{T`&*pL%Hz2B$3>*``4)UPNf*E_7DpZoqeEUe`r>vECzeoF(*yVoXwCX@JT6K8Iy zDy&~;41V5mA*3Jpc85UuSW!W4T>Npv{6I)6n{jQJtO%>>{(9yZ7`q$KujFlm|9_}@ z2lmW@ZC$(LH@0otwr$(CI=1n~wv&$4v7L0(vC~1vHoo*)-`?k(tA4n=}&M&yi4V zj~D5QdTlarmhmC`ld#K94f(CQ%@=%uC9?!omNns6H(kRMk=!R$ z{iixK( zRHI#})qv<`{edfqhaDH}sdy52mV;8< zwn|>RA$qvP8Qu_vEcRlY=~;e18gvE9CG*!` zqX7h`@NmpGt;Jv-+cIa)QqvWwZXB$wB>j=5jMqDAfv8H1`;LfOS}m7{m-n};1z&!V z16gd-Hz;n(U(r*N%x9@xpiA)XWc%3LhNQ7S>AcCYio86M8qL6VjFMBxrSqV@^UO_! z$>1b>5L)?2dsl>_^cUw#WsSuWIr+}9y(LbqbS{(4ZB59XPpM7FGW9S>x(bt+?x0zu zS+F#iEskY3=EyPt2ewxa)P4rJk+j&FfwLMzWm^wJ)8$g74%v=gci0BEIxuwN0$1P7-gTZpVYsxBrB(qdPl^W5J*O0X`L_O}!b*`z&jJZdt z{;GD~ji~Lva*3;x8b2Lu`qrx|9okt00r!SC89}-N;i=DzlA0shB7cbW5lU$K2r)m1 zV$Ripg_tF+{Qx%vH4tkgN=lwnxQUlxQE3e^^|x3ZUG? z0xjiE&IKRgiR}4wC5S^>OzT}GKEUu_1)C(c`e(40!}-1h*e?2K6gpVlYG;HcHKNiY zupqW1rF?pQ&Q$k=$9d4uXi}~U18}C!1hy5y{PJDb90NPm3`sZEG@J zMSnds>qJETV?AOi6mOei`1@xfH~m!Y^-J30Kp_RSY+oFTa#M~d^@LSKmy=j2&1xsH zbA>%lfD z;2!=ct3d^k^0Iw=@R7sNWN@831%Gnt)@jGwkRru*2!R-5(&>Xld}TYBs1|7<`b%N; zTr6+&oAaT%osN*xchAH3)HS~#PNIhelrSd-_Z;K&B|;ViTi09I!V(jSifwld(HC_g z##%EOrhtEQ3Jd)F&TH|@m}yF()ma~BM3M)SylZLoOv1X6bF>E_VvYF`+o>d+*=MI_ z&KR+oB|*Pgye7{uoTw|O?fVh<;DnmfYC)$Z1!fINEJyBQMdrsck$V|7`GWNT&T-SW zu@F=A+J5ZcjdP~gI|=nd^Pa|rqL`mTKC&n+11(7+?Je;tQN*V|@3#xUm{A{ z$ClTD&y)M%Yz@TGfuN()H;ZpS2b7z;vCq>K8f}mpp0Gis&%m2&A;CedN1r z`OqoYKF%0yOh6-H?<0AJcSH0>peGBWd%OG|`J_Y(`gLUEWsIgwVmm;3jb|(syn|`A z3Eb6%p>OR$ayRC@ES;Qk5(jFJqcwnpl3qDutk}oPk5#QgtovjrSwSu6WC%4b5F&Sk z6JiKKC^n;9F+cvx4nyk`7=?f~@)#MXaQdiTMVQNRtTT!PF+@QsF*q?FE#mvZ`&q4H z(JsfH_=IIs>oCDYmB<{)GIwv^I`N*r-G!^3Ts?=OCy5KjawCoC2|uirL4cbPlEzMx z-v`n_gp^t|CE24|8r=IM1t(*-bdgxty@cuZ92TYIi_hYcA1Cpy;g5GH!UpxYGhF6V z@qMVkfnqLXnMl~GG`Xx#t2D4nP>ivwpkBy37U+Q^d^ECmaimqcuVs_okcL~UG>n>v z1wYAk%8V~d%FeM?^k1{iZM9^ zx6U&_nqpa7)C;8VNet$G4yGFwUY zD_lPkQg0Il7|BL<+Uv ziAM3Jfd1zdHIZ$*RFwG}8AD|35d@}Q!A#U{hr&P2H50Kt8l;EHrvMsEVt29gu;j(q zw=l|&8y__dMRC7#2K9{VM(&iPdw$70P(4^K|C)&WJ(=K8etNFkE?cK@_E-=kcvD+H z*x+$iq1?qYx@!d#Cho z(?3{;WESZA_9LWHBA{)+ZfS9`;^g&+d;WaxfRGO&_8K{n$^AqPeepCL*Fi~0IF+LA zk0)$6Vh@SmXlU`5${5u_O0rDUq$D?zb>PQ(5aZT6Ym4jMBh+W){KU8$@@iP1SlNR0 zl!}m0uo%k&lYp?TAMH$+FUZHK59Yx64^hi@u>M&$H4LwvyPoj#k4?9CT!{fRWe26? zO03_(b{qHU%OMuWqh~~?;npyCUm%fU>X*~KmwQXW$r@;sRgI1Q=)6wk?iVT!X-#(| zG^9YHDv2Tv3Sa=a($1Gaz?D63jbWDv;0b_`1ft;<$NWtR#d186`&xQm4Ru zL}GsYHqWX!8%`{P8!j!QhK>1IFPEe#@Lg5}xL@&sEA>DJ8p`)_{OAd;4!&D0=uPpl zPn(X!?KVhQDa_Q3D@&2ye|{k*|0^c8XrtJ7D5fP4iwIU~$B5v!!107cIw;8jvO7!l zY7q76C~Yc>H85vcTD7kr5ERz!FD=MEL&q@*1m4hNkJwZ$8+8*!q@cI-NLHdYW-Q9x zwUnw6Q-^FI%k|v$(o$LMZ12-BW7XLHUX!678zQC3JJ6wxA>hki?Zc1LY07G$L(vJZ zflEr-D9*_D4Bqp+s*B}TYtT{`8b~}NPJ#kmEhHihi8>)xMv%`tH$X_6nj2EmGdptZ z{CNqbJ;l`YxYQRy2ZK=!9?>{((L8uqU(n6GA6HUAhe-IjRLb?Y>t~g!^oWR3$B3OZ zpLm@(pU$!SnA~6G5vSGTex~fgrmtcxfGA@OWSL@0C=M8Zw;hC0@b5UdB2a&x4+IVr zZ(w&SiwNY9?9<@V+Oz}}wH~{XV2sxw7-B+_Bq;{`OAV}WuK`;9zK7YX)PMU&?9(|X z@XxG14)H(#he&A7@i5?)@kDBdZnpg=yduAw;Kl3sreC1&3*#2Bu~Co4@Z{W)-bWcr4KHnKH#$ zhi}5qiVuA+)AJPrX=#&=F+hSovZ0nPo9Q|BTV)r-wct7y{3sHPSX{U;38Ufl+``vM zTZ~nH;^Zml){v|6=aTIEjjj*=3F!v&oA{SY-s=jn;WT_*?Wev+)n)Ie|Ayb=zy8g{ zRK=X>JaQ)e4iu(hZLIw4)aSzt6Djml>jx+Dy>QJRY*{vlDJmjS_x`-bU0ph!r@;9# zh-X-leg}UgW%L4p^SIDSW~7j^8hlyS*u78B8$4wcNXXUY#P45A@SS7O01hfH7;BI# zNcljf5SvFp=HQO!tFy}1AE#Kq$&KdhQ=-!Sp#r+()H*;|QOPkSp`SETp#6J<(xCSu z52@L4mE4+TP=b)7jq!w4y^A;k>H zU;dHsqS3r~rV^E5u-U&&Bw_?(ms{I*=ZCpdK*i=(G6!7UnVVl1+KsAES)+4T76?Yd zmjDht{9jHdkAd9-%PYM7-7zD>y15;bp4d&W*!R~sh~R72DH|(M&|Ws%UFw? zmHm@|zW7gvg4xbT+7OZ2ZNx~YhZh3m5cMVs7ck#3ZN&7Q==b5aBwx&c_ULZy#-16pm*gjGvm9mq~SUvL`rB7Y$&*O)9P5tJVcWe zPAD{m;MU`+BeCQ&i6{(JYQb77nZRuyB;QSt4A+LPv9x^u3?%PbryrMbE3U?5- z!`>+F^^W|4@k3WA&L_qQo)h(NLZWzUMslaqhUI)#o5pZA@Cp;ZY!Y$DGKGgYmKZzzOR_qKwpuYF<(smll}GE zeEmn1@cUaBfR6(K2OaVavfI?p>g$-c^Uf=lj6Gqq@r#Y%G3qbo;~<3{ia$+eZ0y5s zz+%W+E_u?cwlFGnsap{U@^i5~$&oGUXwkll>g{PJ&B3nnbZCkHJOg;tPcF+JD{Uvj zl_5rkJ|xIda#a@({u`+B)O#x1!1{!Fap+kvn+n@zjbZl}f^Dq{g@p|)4I#LMAcIao zY#|pg5+ar8%{e!0MT0-QS9Ghqz(i>99YdPQV=$v_$$! z6x#1@x8w>C2PCj134Z^hL8QOVo$FW0Iq^jIf?s zHqE%}{t@HSjw@w)L4_IZca-T!vD=-AhJqoT&}Y?oF6&zKk32janocEa+FgbZG&lDt z5ydJ%>9ai`OCuTCvB)hjj~@W1;>^s~3T*gQ*5F$9LFRei=GGIcUHa^G{ew@Aw)@)x z%#x9(5q`f2gDzQ>9@_9DF>Y~7X0dn|O6hb3Vt0_%H`MtYKPgLM^1tFW~aFgH4V9<*h*ygkG8JVMOmyOI7Hr8F8V_qJ-@# zNgrV+qcI7IeQiz6DlIZd-M_ThD z5^eM0>V5=~u-xj1?7??&C3$yB8N-7S*ocD1VvAq8#ZIIJS zBn-JWigb?;5JR|&(2STnj3FLX4!s!w$MM-a zMj)#=B^=$WqBB!n?j_ynt33p7Z2GRGLw6#+Io8G~?pltT+rO#B7YFC<#OwJm&8FiZ z9NY?MWbM>MwlQKnpoxoD1xG7GD)as|1b1oeHLAGWxLJon=jG)%g5|xFd!u`ykT~wS zMpxlxAcGsF9jR^5hww=wg9gqNdCl@WKSe&IIVAk>Et$8}e%uX?LluLZ@G^dxNz3nL zqd^oTlagm=t$}r`qx`=-zQNM3&cmgv@N8!k1tw9{i$mZ1-gJY|NjegfR{q8w_bFy? zn{vWuVRmFUB&lhT@cH42gjl;8R~g2jriJRYZ4F&x>jNN9CD5Ax+Q{APdYCWZCSf1U zpaPF>O#P1D)l^-I+Bc*HI&trXJA6ym5~_vBP|No%Ki&5I@D1=dIpe<#DHLYUgH#b- zE0W;D60LBW?}V(7Xr=H+U}pO%UK8-@m*gp?79rsTJI;w$*#hblnQbtl2JX&Mo3p7T z2YT|V6i)$0Bqs#8-4w&~{NP|J5@r6y8nmYkS$Lu;-Hx*NjQ=~<{~7hdzynjmr?q7^ zYBq=wQV0|`h?5ff0kRP$uS-%~=lf=xDLMvX(QO%if5w3*BGBt^7MO~|>n+f4HuA7N zULM%<@jo{B<7mPS_|RG;J&2;u@5po_ZZ`1D)+zc;=>r8@vme{$0yFbM+fnn)-oO(| zL0MrjjSuD>BxDdw)-T4XZl}cZcq_Yb%{?qkX_A^2i94UDFYaImYabp``x}|wotIRD z|86B~Bbq9Sc_Q0Asb@pt!h#SqxM8U_CX|nsqVATQsH)y+(@2>Y)4X!YJ>VsbU+nL; zU^aA>83-jdWUS~KjA)o%I=;)R5`7JM8%a=O7QRiqIouYj3Y|RZ{~wMZ({xas`{Nb- zSLaPvW}{YR0t(+YDRhI?jUv!zwWI6`5i~JG_=Iu{N?^7oSVDHczled`^DPnrCK1+6 zK~FKn>MF)mHn<$Uvl7jm!rI*t~qk9ye^~!>kbZO4vXSRmj!~Fdv_&JrY$av?6J7m?Z?9Uf{eet7c|O3pY$t;GOP{!r3e^UbyNqC3`i4s9=Jj=wiO^DFQt!jW0>^- zB|RaCN5?4c1Fp=AQW+LCGT#WBBR7W-bRs(vf0YRF7xrt<{)HwH5i~ij`|k&y1?u+& zc`Qsodzthq3=|9eSSrqT@=t|#ty<9PrVmn(fwoB3-9eOSDCdo{{gn|vV7Jq$crlO{ z{_W$4KXqgGQaHXE3SMnBGtUnKXYB@<{Rzp@EGRy*rU028-XqgyxI(ddpFkb1OF~6x z=*6CSi(+^2g8O#IWwt^Zx$BfeBfR%p$_`bHOs)vl_$aIrGb*B;9oG~{asfQP$Z<5T$`Fr8C9+U&>_ zlq_>@fEzWD?weVHYg3KdPv$vuYjDeU50cz5@d3RE>8;_X@qGo~`?_rBmuBA*ACjk$ z18;zKp0Q916$vusW(h#~jh^%m$vi6qdLlK7bpTx|s;pq>*92qN)4g0fw=);&^WiB;y+q zBclqzl6R1RFlH-{v7thN($UOtVn_KF(_Lxfv?{?sna1L5=}gly2q;7ni7Nh%9MhP8 zZ(JXQ8T&Z%sAODKC?j}B!P~r;VAfo2VKB8Q|43k581!6se8-Z%3VQ_ z)ED9%Y37H;k8zyE@&{S7$^c7-3WJnmn$8f`aHmB0vLe)qB3mI8*3d&|rwh#B<21PN ztkEnbvABN3%w;6kGb;pzt?|uy#m=RyYJtK;`5`HeusnQIyGI@mT4Ao(^kaE`WqI&f z_5WK1!V7uYT7f_Tsj{mw)HRqq%`Wju>pmwBAB@eHc$f2?Bokcn9_O&0to^N z>H{uBCYCyGSDHd9iOttpGo0cyOU2t0r-6WcufJw%9t-(J_Kin_Fq1e5w3UqR=+OJm zWF2%Z`)@ONxq^>MkC|GGFYnJc6llH7Sk`~jj9+nGrwoj2qCDh&Zy zL?Kvor&Pw2FRWULsq5W}^ig0Kk-pX0k^ND&Sdcvb?K`ZDii^;tn1e1t=Br$@SvEIr zVI(wzD>9FtYogSOvidxOZBiu2&)~vYODa*-^nRt zfxRpeBW~MS2r5iOgv=rsj*`t}Qc-FoLDCn=N`LgmE}1)&&I|lR8TPJ4f(scs?CzNX zIq>8Ju)^cL$2RI~wfcDksHmS*o+@Br#d1#ix&O^buEMW58;26iy@p*i!+Nzlz(N$3 z;KdW(-&>yiEbsf`GcM4)Rz$G|{(RitENA>mcRz#Go`ONki<%yww{oEiX6wR3po=>A znX4CBt>D>D$Xr~yAT1pfGS$wsu0~%(MTB>*e8O)6epUuf@{2op9tsm9y;EtyWMDmx zpe92fAeecl)R+l5OWj%xHywA26n|IHx)2{JZGW@TFTdwSyZ8gk4cvu-yw)5+!;DgQgY?MG_%`PE_D|M*R+p1nADT z=^N>Otlc`5_AFoc+{#%35aZ&~>7uZzrutF%eT zdys_G+mN=vq&j19_sI(PfI7?!mA`~YDgNm1m=GsJa}hznB?L1iUG(3oKu(-u2&R(a zi-i;u{p+I?B~+4_nKsD~P% zp$DGI{<+UAG@e2+d4$sv1g4 zIQ_Yd`338@wPSeKns`p>*^Nc9A&P7d-O6#LA}-Dr;_0cX4T~k- zC#EYbe9C?DXTCA#wW#JH`Bp9P2VT%Lsxwb1`w$F}m_YFYMIxgpk^J^pKTjl-6!zrH z(y5-xn#{GZZxc2mPe)MeS52m_R%=bDKPv^`Ca_|L*C&{~D+t-?Pn8#&gAn?M(MAMe zEZ0`YpOP!2^^y`VQsS?pn+f_Cb0%Y>W|Jn$t!x&}d?8ctxn~Bs#Mw|?NJD1E8Pr|BW|4KFDeD>_XivW_tii zuDQReoXgeQfIU|3@YFckKwW?z=?7Pf;Gx|0UT)*#?9gnfE)M7HRK+)Pcd($9?`ja7 z%s?F#DC5mSXdjd@aY&E0eAd?zkW-s23G+z}9x6^&6rfz#02}tr{AWR&g|zyabEdO~ zM%t0pbA@~x8;)|?Y>%Sf+UB!|dIRl$V%A4ugk1lX3J5S?p~OR5IhPiM2Z{PMMItmv zu(MXKv1d72vY$dqqok4?g*$P>s#o8`2MBHkq}LsBVB$@eJ9Eh%ugvvp(Oja`8tNjjaJ;*@<|+ z$gEN(_!;J+DhPvJkcN~`iK2I%nK?x=b`?nl$cD&^DYOQ(KVK9MrD{45hfigg$O>%p zgA$sdUS~^w*!-vGbfEuD9!Zj~P3{0jxEKbS#;)5t>bT)v5{f49O z7>*kcDK8YzlkFEg0nmLXIyyQ`%n7r0pN5+mY=TWpJr@4%!q2hTBY(8(VuO>>7kemT zabifh7BSB*yb=Lvr~Z|t{}XXK&~EUrkMH|<*b9m0my}(6m0I9V_7OQ8ZQ_vxfmrQ# zM54%#pk_q+JLL9*Fm|ree8LDs*}`?_$SpA8=AWlX`w9|FL;G3fM&PJ9{i>1E?lBeE zN-3oH_A&q0GrsyJWce?cv61gv`$&lv#6xd^EXoaRD! zqKU0Vg}_7c8qcvSvL`?Jsq#Lg$~=odWNn4P$iRW!Ed37%1s@#=QUYnlYvm{#B!L*k zK}q|&x0d6vAD?2sdR1B@{&y6)691(>(Om1%%M>7z&MoJ)+#Pu^c|uy7tS^k-{Qhb5 z1jg>C1>P+kov^Yzc9jn9?Ajj@>86M>2{)d2Yhm$QiWdPdtO+*YIkTWh1VPxkPJ3P0 z`cs{sWDV^Wm+(SHrRa1|L)CQ*fq2G_{1!pi&qg5cxXS-(o0(gF;4Nik(IbbaL03p(MsQd(~f7ZjI#?5`&eNc2oDEm7o`^F(~Joe}Z{c!xA z3*0n-h>{f0+lwI`^&2j~BMb*RlZu>3fCE@JPO-?Q=~@}l1NhcNrfQ{d_lK0+qe6Q2 z&_g_y^?qiAoPsdqSMC6h-@ZLIxfEMhG#ijFY{%1_Xe&8L=P>t}*7S__@g)c+qPCMI z_|>Y+Q*TRsBw`eh(tVZ3393)I+sX;>L{{+U+SEW}N4La&e^?bHwaFPa2j#!w^9{Gj z?(qzh50xf%|NGebVR^awA*f=ykUvsd${Jof^Jmfh!Cl?cu)%e$Q8q@%P`J+q?PVhM zEjU(`ai(2sCis?rX=OgV=!ED5>_==M&aMY$p7wGqZW)3Q*=*o_7&n{Z&EP3NsOas! zvO1J+eGxXR#IUK_2!&m>ZZ!C#(V@^4F+T%TUTeS9%`z*`{C5iv2ca}as7QZO%FRn}_*J3n%>=Pd7G*B$n&Q4xnykezk{F)>V%2 z&)qr88)f)d{uu*&`6@MtKjFv`5@`9Z5>@ERidOsPoLK_?7obza+4`YW6uC^nPZDrh z-#J?IwRpS0ZCXt1=#w>*+IKtgH$gi8MIOP|tn7pDV7`@uu)=-xI@I71B3^(ofv~eV9R(aTOY#ju^nrT?^ba9Xsi=hvgSferaB@P} zUP_4UlS5_`6@0_ly`{NyYtLQ!LT)tZt2f_F*2lpk?))|7hocM*#^j+EqAgt0W{MEh za+f78gi?9&SiBbO(kVn_eofD~ll}85L*&DsOpH1*{05$Uw6mCDI`II*F_KUR%^LQI z7cRTahacs(Xm0rxR)1@FjihE-2lP6HwDk4k)^sF9i;2z_rza;_LGxVY6m_dk>*$Z3 zxImK9K}smUNx#W{%v;9Fk@14*i0^V!^SpurT?(xaBwvKtoEFZ{~#-i+XW5Y()N(tbd{FxK1jH zokp-NGnlzdRr7VR6%-#Mq!v&W`GV}%TZxH@LDKa-GPOPMw!f5b*JrH1Og3NKt*otu z{rv?jW^$swY%K1}{7Vhzd7)9~G2%u6UrT|3523l%xpROO#l~e!{|0*L)8pH8yR`1e%LCWr%~ckGhyo~lnJ4?$!^W%NLI zmAF-viXTa2LfEO@k#W+bz@3?ICfmLJyG-g zn4FSVNar*8*iW_K=JpQ&Cw{QkvXoh=nz8rq!Nr_KBt)1$c(+HyWUs-pxUTwe3Oq3H zsuDL{1dEVW^;IY8$>-nICa%Y1P9#U~G3=@gVEBNgVNfW=A% zL%VXM`g}15eI`Seb{3K_S^VRo=;wOn^(Fpy-ixC2(7tb8P#FBGyIR&epM3sR3>f(G zBrKS}Pcn4F1-vZ~3KGkm;#|DMwSJ=IW}!v|`cl`W zq^sFBV*Jx%A@P$9wPsIU*Ah>+?Y=qOj~T1q5iM)<(LPA+8L|k6=e7$W%%{Ju&Y)Wt zhw){2p-?hVBWb%}VBv&m`E6>o2aT1+0$*L7PpQ3P{epw8_V@!TdL;!XJ0WKgS*yMj zR5!*KD=9rV@fD1&&Oi(W%}nEzjiYKwyriR(DyF5;eAGm44X#FCKeV(OV?Re#vHwYq zvvrYfq@ZN*YHb4oi`Rr~ z&&T>T)L>bB`8Xlqjntv{-Gf!PI&<^v%#QUmHzma^QdCb5uQ@L*&ElalUMlqCae{W))~Q^-;60jh=eJ}@c=&V?q}8RwvEF=v1Dq})AF|sUt*_) zpL&*ahY80ut7Zat!k$Mnxt&r2shavxmjRcrkb{~A-Vnur2ipN%q{l24eE#u+BUw!Ui2I}$Z^}i)VA+M>$me9oyaJ0^fvi5B)?bzUYjlQ zK;Cj?b#E3`6*Ems*^xo?zmS*~QyIp2mH134RSRMuLKU2F4%JW9c0ezMle(OkP5FQc5II#5keK+y)@=r;ORnh95sk zHq$ofkip*A2Jr+rmTeww+}7gq@7$#Jqed3iFex<$l>5{U-aT?67O8OYt5T|ms5s*) z(Q7&~CsJmQbOj)#h^#(~tbbJC%m<216t8Tjv-ZRKPlr$%YpmtS4a~Ty+yK6r-7R4>tj>S{f*mlW2FIQ ztSs&z9i$lqRu)z1L5;H-!?@r-QHxaOK%ylQpmtlPPd2Hwuq#&}ev575$)vD_i6G3Y zFxrws0jdv7WAY0r(pPP^BCiZz`w*)|Te`+{ZDZ;#cj^JBzQcyWPjjA`&lDz1jnf8) zreTZhl$WW@a?`WzzCKO7Px_FoD^Ad?>#rUc-pI@S>Gby2!@~e9;}94kUww5)=tz73 zikXj#Gx&SZBGz&F)=)26Po_1DA_*ZmXp~+pEod!2YWr@@F~^{DZU`^D(!Qqm`~C9t zO`>zej;80^#8i##pk~hvdZd`1=cc>Y^)Tjjw{364P>C4d%@}Ff$LGi8=iV2cqELHz zR9#a)?0mg#zjfVVkoU4HcGGkn zoDn$1WFC(VKW;Fma(S2WY$-@1y*16OmxDZpNoHk{>%|Jw4cZw^+Nfn0!gDF4(1Lpd zPP8`Orf{a#Q({*mri)M;am5cDl0XRKv2;V|-5m!n6+}&)V8X)5UWm|4W#89xxF1?Y z%Q@*R)1C#?J8LsI>>T$d!&yfgHxp)lyE9IpqzwWglQ8dvKLE(F3fw_Qj{PSSM^t~p zr{o@c9>d6jLaByj&toq6`0lcB2b+Sqyj5tI(iqFHfC}Kntf$Jr;%aA@{EGUy-&?<$ zB}-UW@88YK542S*CSY>LN6;Y-1Ink-3?lN)~xRHf=(eY`CL)3f6; zYdEMMey8Oh0u;+RW?~6&ePxYO6ez>{4ZD_Uz-P~0aW$wUn5 z7x4#Q>nH&>R7TYL3yLq|kD<>w`$AZ<0?&9N7;;zy!S^w%Z;g#slfy;VHobBw2>f?S ze9wQ9Ejll{H}{+eo!kLzSh| zNw)<<9zND9=_|^JF3f#q=!%Cla*1(lekVfRc4tEkADV;y&Q}fu7;V2}v-l*>|p z?~6D+W;poR7%jA`IZ2t7_tj1e;B;Yib*RU?i-Sp8MNSD$-MQLhi(iSsHc zK5DI-LLf%UAl~MuW*JZ;KIg$ap@Lug(}cqpZJzP-VOr~EW?#anuFBEEKemF&|7iPS z)U)^A&-c`4d*t%t9*LywgW%TCyKjfZ{sf)tC1uXbaZcjx&d0UhY22oyd}#lp(?1T~ zc4=n_HGat3uoE(2JAks)<-zUHIl12CH_sUz{gAp~4_JC`0O*JBXYtc|H{xzy=SAP& za)Zd;_uKULlbgB>V*}U0-&RGpMUf(|T5I5$g5qdit$381cWt)C>^SOSp8 z;lFDG-14+K2}|mcD84{Q&Cjaxe(6Hz8QW5<1uS32DAG4uzq6GFoim#FtGAEW%Y7)p zr=V2^`_SmMq}*OPPqi7oTRFL09&~ea^O4Klw%+$2m87U;3JEx>0Retoq2eD9|9t+| z8f8|bRy|zyYiz|JK-Y(})ut$r*uPnL8RA^(V&BwwwnTL>GiURg6x*RSx!ESgWX<26 zt}S9Wcup>^p%`7uEFS2lftG-NiAq_{t}ltT>p+dvI*^5c^!!MSz2 zzL7#Yh{^u}O92-YQ#K+m#`x{Z*AVGxiN%^MWJX$iGFz5wkNxWQf}26U-80f`wXCwP zT`3BLPBJA}G-lN#7g?ar`s9JV&8N^i<;oN`e{tP~RqQu)JR~l=#>l&GoPP zyy*vs>%=C6770d4DVG>65RMk#{uFri>~Z35O3%)W#jSfKtnvC3^m@83r;Z&hC~L^% zICP$>-;_&og2?-RNc?utCP*&qg>`y}xgS+-uv5dD`x})Xi<=2EPWNcfqSg6XbEXgM z7d@N4PKO;jLjE&*&6ZMV$BRVGc0I(gAnIT^!oV)-i1Gd#rSl+SPZZRLKkk;-hrigs zkM`Fd!~F+Aew%nk>gu6nME)?sfJYh1NU``IkMZG;U z`-9PpWxkfquRa^>q(!JFi}19adbgJEB|_}(8<7QR9Be0itUpm$y?+7kmQB+4+bqV_ z%|Ze`o{whS^WKvB9CKtKK3ZfWmnsFFn z?cC?Q>*g+ZXHCDd>RfyHM(KEaIdeKZtdWml0a|e)hV9XQZgA0Ve-0N64kb=yaVnC^ zSCLt3;wI!r81pwn4RZwHOjTx%;1KQ4L#mM!K{Vql3=BzWvN?;Yeg<#v*B4&6x1!4K zcFj%gT~3Q`5<|g6Rb>X$IXlJmB!2looHk7iBn&iBzSuRXCWiMU2=mfhOZ38BwzmYC zwPao@%IkU|U3c_YDd{nHLrlR9moDovr~_V zD8*-%t}APm0C%pUP-jA1q8Jj}M za~puu^SFjs>F7qXU3U2MfR`mI-?uapM)4R5<&u;Dqnyvd(HX#qoY*I8EXTSWLFnI( z+-LT^1gTsBNL0RRJKFUp4AY>m-gq#!fRZ&)KRC}Hqr0JE`)zK14S}>N=qN-vslGCN z-5MgZzs-%AkTK-e+Fz`wPm;+0t~6n6AkQ}}EG{I$Nn`8V?&AS>v#E*l5GnN1+L0B` z6oUU)i4^Ol0?>(GKEbv7u10lC?VVekO!Pb!n{~u4$+h9c)6)3@mfNaYYVuEpv~)}X zKnmre6dAZ3`ch%alb|`uqm}6nYMWPnE++Q+@x`?hrckHh+3pJt8u?Y`x&^J;@2>^^ z0{A*DzXQUqE{7LdHZ=oKCcmsHE;&52G0b2|*DDd|T;65~1RqAY4qi5>^C*5}iU8M# zIFF{Og!)Fhp{PR*J+}=n9zINC)pHddM$Lse_264dbSq_JgWz_gSQWr@SsTkA8#q5m z1Zlw_@KWs4lfx{nPZEHE!lSpz-d`hLBrp^TRU^m#qKQ!|uTHJeu&z)(H8yo+ElU>y z&U1*B+9A(e5k>;&Cjmtg($6U|ZF`ciTY$sYt#Kodgt5EcqR9DbVQvAW(313P70^)& z;I?vLVd^}4^VXf2P1`WcI7ekWm$c?hz4BktFje4-+qLPiTTV3oE;Tm)`9b< zGOAwnKJJz~FZ}Qk zFTnEIjg4Ss(r}}PpPGA7pFOegxNUoBh6XVC+hIS#WZ@J>J_hdw{Ld7@bFg)}^Q+v@ zmhSXl@j=wK%~?o`3Vh1<$;ZVXPQD# zcS3HCr{0K|O>$3vB@h%_Vg|@`GHx)c3P6v*;8s6<()`0UIL(P;X1a%X`|#T*64Af< zJ_oX~u^H_Sgnb#Ad(5V~&DI$$E%`C;_>zBS2c%Ei#s*bF2)O*qaD2Y+&^85$}QcZb^9w zHBWM=HjA4)SS<%&%smA-x;Ta+ey*gsN$$iet;`3zveA?CW%PLavQ&}zA>4`#k|4#L zevf~A&!`*+UMCyp1XshdEHI0Szf+MlH!y+>Q8pI@c<{TR=z7(fnLXdmE$Rfp*r@V3 z*kJGpBt>u?QgOjE(nt2MiHw*K{uPxtM0ntabiAY+oc}?L$8JIu)vs`7r6o>GC*m?^ z5-HLir?rsrn3=C)V*{y{TMw72}g)jUXdj@QVno$13htJ67qK!2%bZs1+B z86J;6Ty57|b`5VbodP+wfI5wx?2Jv=j8(FV4fA#oGW-ve~Kg z``5?-Y8%n8E9~BQD?@ql*M@B*r^q0=Xf-muXmGAFVi;FyqXYl98EuUYpqVY=z&wqVwxOFE}CSsi#{U;RGI|R|yPm^RBnB{5ss@9m> z&w+*R7eVh1$j7-~^`cqcI^F!!7C^9^&D#`Sjb&Yo%Z}jIMyM*x-c_oqUWIAoL`NDN z1x)quDKEir=tgF{`^&dCXJh0%?O|V#yuv&AivE&dJvrQ6)1k!a73JP|>9v6|#k@fK zmTGD^6;vTqTwoZ}bO@i^2nU}ZTo%|fRb4roqH9hhAAld)%hd-Z4YQ@F#vDG$7rA5z zA5ufI<5z^q2xBZ$tfl{iN9%OvTSk;r(N8g=Kcla>*-pO>Nj6xx(Iw-#AS$KJuP9ZE z=QQL8=KIoPEKW@lop3s5@zc73FL> zXsVm87d}Is=XOJ!w%9m17Q1BNMZu%&_y4+OOJw`QpxOWRvX`Y}M<<@b!WHwAMeq78 zdi-tXdM@hqwCQaH*$Y9Ib~)LO={sBt^t_mqQsRg>bE(;d_}0*U#Z_0Y0|&=A2YG@O zJ>>WzT!y;8uIfDn0{(q_vUv_WoH}%#;grS@{NAwLXz)deG-2HTrR>|JeFfZggAi z$36lL=#Wu`?VBqOXdghOVX+NK*yDOI1_5UFAlHHPGCEM)QN4Ff&oeRt< zvAyAcI^-13E4DDyf$5}4Ixf%m|Nm$@2gbU(u4^}JY}-yw)Y!Id+fEucwr#s{W7~FP zqp|g!+|T>{gLC#?d#$;~oYxpQKpRI6pe3Yi%M4MIr)Um# zpG|>$c}~JXwMJc^2(A-Lhl4vh^eM_qlZqaAGPQCEmNLz7sE~-#<22A`J`#9+Lu23a zW)X|Q8x2R|TWS2E^?AGfF5K2pTkl)>bc{-zE)&aIWT@Giz`hh~S$o-=v_&LY#hgwT5#yA)Ct@Fr z%?EQ*5Ek^i&hXfHF+={xNsGF(f+AH69o9G@=E{g18Z$9G)$@wBl=%eeK+431J6a>m zLFMJxGo9_R(^b&t~Z+w~tiN*#n)$cTe)ar8B zWLvAgEk>PTHIRWa4yD-XOK~};vh6FF^sVTAWeF9=crl+zqL>4oAzklz7@;9V2}sH^Lzdu@7?DSIpJTzZs-9rbXsy;A$i=C|7uy9^~H=Mb{*W-zyXYx&G5>kdp9wZ4UeME82}9rW-2_X2o?b)4|*XCko#&C${cAl3}$=-Rfip}pP< zeoSbV9{}`)|d` z3M>txg#=sm9@?eIwoh}fEZ)GZ5EYSnyUGQ*5WzN(vKgZ|Dvy=O6Qd^Q2Yj$BA@j{7 zGwbkq-gKZZTb5%)F=QaXh}0mFZe*$@<+q!C$9Gb(5(BcMHrQSHJ}~FKH1H~daxx5g z2Fszl4@nL?Z`ietaYmN>+6}{5{ zgC_-b>bX5c@@omxfwadHk2i+NgkzOzaI32B8ZlzAWKS&?FV`}=ngaa+E|^Ye)^5Wr zFD}OKTC3WqH@5=Ny@yt)rHmLUyulMspbycCx*5%Nim)urb(Dv=FUCA~MyS z4{MUmOpKA_SqefVu*M_){V&{OucXIg9EZI&6EK#i}T{N(#cT1He}m zLu+Epy}iSw)7q~|u@sQpMV2ZgCQaH_uc&AR=39o6wl;mcO=alcI^Ksf`@`k@AW3DV z7cEc}VoNJiy-1N3xd=~)a!#&b^`LqE@p+s3iA5g(E&yHQzUfKX$N=I1O#Y;zqUr(W zn8`{kLcn4etXvs-0L{c5&g+IVHb>FQ|DaPh-f_zt?~|hQO5@GD#|NhG1uXl5&4Hdj1y6{g0zi7HKNxL7=b2_zWO!vHCHtsnPJ-#+KolO8AQvDTa+|ZT$U} zx>xT2=o`f-9glt>sxbRB=`+Yh!dRN?)3CKVR&Nfn@=`N_mW9rSct;6|=z`&#I$Pf6j`%>YDB+8V>W+||YAI?&6XvFc2!b^i zEDDPoOzr+ON;tuxP0Terx6zU!);{E5$iRUY%Nic%`9A)%8*cq)-F&+6Gu-pe6dgkU z)^Apkx=%@N!y#%iP0%p$C1l7wozsh?qqFjiRq)V+O*S!`)tgMXgDoHu^&`Lj}(I_o0g^KSwwxTD3Cdrk7!4%vV$4` z?HO7XWE;M>_alUHfvw!1z1He~1^0|HP7(=cUnAun{+ZyVx}!6h)gXU3gO~mzXxuX= zB33p7piHsZZ$<$#6#hkA=&Puh)NhYP4KXQ>jgB&{`Y-Ya?l z(Y^bY&vFnix?%fkPP;qjgsTRCH@GfkDOFO%m0Ke!3EtSkc^qcx z=8-1XG7}P1nlC`Je!xTqqkMr69U0=N3ne~_)461o&wi69QCoI_zT;~uONd!|Gts-Ft_E*(XYkt z{&3>Dc(=x$G-o96?Z#W~Rt+JPqwlHhKnoc*At@pK@7vDN4haElMkGfLKv;nw16+3eC_2$IR?VXNLioco_iZFF&#jRxZuuxybdW|yLwG}aG{djt zj}5UT_>IJj=CiF2CA#6ZV`n{ab?54Fhuq3e-GT7I!j@|)G1>I;^OX6kPlgf}n?9K_ zX!Yzz=Uy(k?v+DCxlriocA~E|RztQIe_7lHHntoygT7f3M(GC^ks=6!lSULGp!3na z&0Npkir~6T?cr6Lh7FK#+mRtm^JSLUE}+#}#o?qq56b6JG$w*mOl8T-J{*u_iBur4 zDQa}ta3W+QWuvDw)HA=HW=KQLM>XMP`&~!sZv$<{f3V%5ppnB#IndM7|A_b6*Qump zuTOT5xu6#@lz&Xy*Z+HiD-}QtS6K1JJk>lkY`KAwBw8cVLWX3WY1Y@&X&moncy~N2 z7P)7W>Y195G>Ry)GM=v~Hzv2D+*Dl9yf4js`PY*w-%Sn&f?Rk0NBk87YCSY61y*&? zJmnEyuK7tLxnIP=cq%UZXrAD&nQlKn7CG}MIo`jN5o2fw!Lky{$g<+K%UGfw`})Mu zLB@9E8$r3X%E)fnk!z_G!fQ}RI@xC{mly$?G5+K*o$oLY0Uu`|l~LpJO#3Gx<)c85D2kpSNIdj%s{9h&1IP`cs%Gu)k1Zcjz4b z^QCh0{KaL#AiSaZnHs}WdE3B+MIT1*-~~(E(afJyGqH^+apJ*D#VPgh$xrI`*h)7` z>B-Y7MmofOzh(SDbfn-mf{GSd_d#DEpJ;Lj>A)V{{1~~76|z~kc>A~NB!d?huqv)< zEcskG;2&>LhCEwUOP?F#{|^~B)c3PNvI`Urw*9Y8Apk}aN$7hf6^Bp?39jT-!l=|y zx`nx$#=EN1??^zGM&;1Cu-ueW$PyFRN5}0HOWQP(1m!QM`_Vzy z(xrFD*FAXj>mHo=dkVA4ej+&7Xf5TktVo%Gbc3o_i!S)BBE&3XWq|Qi5sFgwG!~Ja ziCsVuogp(_gmPc(P+>$sxF>Yri9!Vub3WG~n29!?>L1h#O7I=dg1aaWj2O;jRM&(? zqwQAkgHJn$l|I|`?CQDvl+1;!H3{t)8!yKRRMGIgxKC{k^kVZ|P$-3XQ~@iMEk_e( zav2=jth*7n4^1=991)()Dk|vgs!O`CVyOm0Ut*P?iG>zy~TLh&OUjd-*|Gn zjCj^NK7=x7$y6Fs|GAS){LRgcf{u=jL@%&EWm{BQid(u)Thme?j|2g)_5m9MOgS9? zm%>c$#OmB$S$L8C1HP}7cIOW*Xuekk;3m(tG^V+{#Z>#nkvwFzOP?<+*h!@9_-(3{ z`_IK)U8NU36LY@uI-(on@rK&bWp>?J$CmFOs5>|CDi&w^?m(Zi&wvy*6Xt-m8684B zeXW?`Bin&aOhi#PCkh%|YP8hXtIVTggE0wqP48=yA<8_)J3P`=xPhW97RK&3&9#r; zrb~M=U3jKqWXPH(f)pYz488KwQU&P?+(>8MU z{9MF&Jm`wiv2IYbk?NFT2m|Wx98bnRPdlORqBAFSPbcl9chsb{Sy=}TG@(l%V3284 zJW?&>+PA=9MFL=6-D>rf2T{lZ{?UqdLY_12IrV+|>qhic%VjwT^f5k!O{tywdX~?t z?dYqi{ZvCcUj<=%zlA>Iiv3<4ye7C85$M0ln(pUAf)&7eF@OlvEA5YySOxirHw>J5 zM#A=>_BbuFRe0}JAdwSuB_jZLu5fvFFYq#z&$FQ@+vLUyDm+mlFVoNh3a+M3Ey zYe7wyi&yeu{KrgP0i63n5d%wT zjgm3exvCp(%(uODqPq*eA~%*;^GFD@odBs*x-0q~TUP2Sn_!hRcHfxe>h4#d)dV%U z=)ItUZ|BR;RoYBg+ov23PDNK?v))u=E_762i?VAHK{l*xNtgsCfKcg_k^DQ56}p}& zHAK+uY;QO=bPJG698(_2#+};_Ts2>C1V#WA@MgZkoi9$Dm)auIB$A6u`btjB$%z~b z0Q82^YB2?I!Xw}dG{j*L66mW3^kraV@tH9idE-X4${Oxjp;EBKK+m6A2KF}^PAqNs z6H^z)|Km!>&Jh4Zuna`&ow*CR!-Yz(O}B&Dop$zy3y|moPC0MSBON1)1Z1;z`>ifD{?1JP>&;;z2)=zcFK;ek zjF{v3g1xt|qpNE>VYuH;l;E%*FOFlr*lFp`UfPgJ|26R5gNXtp5w3ze&UyW2#K22> zwjbj;9F(r3P08={Erj$UV4auMNdbwHlTSV(I#{VwF8QrZXq<4i=~Kr-lM`wkH9MMn zkw|9E2OTSkI}!VZgPlmllab}T7Di<oX=K#6BB$5)EmJhQ&vu=ok4irWXD| z9`YH)yhGdmMSU4FcPX?U4zJV&4vd8aJ~^}8a#M2x676|BaLoKltc0Tbt#|d(jz@(5 zO!~Tt{#sGDp1g7BMv4AbNUy@u9Gl*lD(LIl)}MMiA}nLu}_(ZE0oBdQ~{ z`DUU;$%4#m+33onI~g=e8d{a|(WUGllo#A(MUc_IG~OeVW7)lWpC%}89MF12OzODS zIpDYquP_?goKpHPRW_RygGSigrI^3=|A!akeqDy;jg0=>5rOLrp_RkmPfR9fqlNB; z6dTGqf+@Y(W%0H`Cx)^cDl5`NSdn_T0^^=pJ!b%m(Tm(?j-Oee0|Nj7?OiXl(9_wG zB5P>YX~jDt_Wp9KC0?#Q1|x$q=q6bDsF0#`4_il)tviH^K(BLR$(b}S@z|d;(Pr&) zmB3Fe}ED42HeC8QUCQekeRqo5`UkbYu*Aw?#Olu&9b~Q-58mbjYKM z+D6n(%MaXGD}Fy#+*b=r`wX*I7GV2`k0Ou=i#jBKmyhtC7G-NO7fuCVHFTPhns&e$ z@0vYek-K^HOSfMs=$8+~`781G2u8{%QFM@-N0JM{$rDM&5x(q( zDLIN~!2f?}$u=s~%1U^y|A5^wG}Li+cXKVmZwA`Ib!(b5Z~8!v-;Lh{NN?!}JU5=! z?m*I5DKOnT6eE7>R|`4k@$VYF7#iP`Q0QuhsePVFOn4xg%S#T^lB;?cY_ggBEHSou zp+kY-{A^>QxmQuaoEEZ~x7i=SA3T zFO*IE9T?FFH`N9e`1LQou$0{JTcedb&KNS8EFoy4X zpD{Wwv)JW(M)5}mlDIPT!j7t_V&tuio*K+cN$B}MSh2(pbgXRdg{vJ`^ppqSpfl_K zwGVH{+)~c=u11>3`yHx;2|E|zorGU|__h)bCRSq(g!itmcumh^t*1%)yqi+Bb1&ppL2=Bw$iEIIjVV^>@T2($r{f zz%ZWMOT(KSjLQwi;wFNwOp9KXjs|_tIN>HzCS!CDM8qWEMqBmdZ24Ff(ruwkADeuG7*cO-cI1%*4qR*31hlk#Vuw0S< zKj#KrO84AiY7hrLVkW5I007|+C|Wl4?HcyVGG2+{`U0A_-J*C8dspZPDeG<1g#xwT zn3M!;->}2&WC*h(t|7{w9y_X^`kie#d~Glw?{I|$b3Gx}l+q$>zQK&o*A}vYK(xY> zeV3jUN-P>vKnHb=>Yb`B81telJp`AhIL#m>BcUrl2FPQQ66p_YiMZ8iB z%l;if-UFf(`Sn)cC@`Mtig!d~ni8hgafB;5+q40Rwj2LPvwf{{zF6Q6;q!s=SOX$e zD9~*hb&?yn^RA@u7pov#F-UH_ZbZUur({^o$iy4Y*IN#RfQ0(#J<{5t?^vgZ>?uC| zyf@48m%<`a-}i~lw@V17k%OpLmkdhy@mOKCQ!z0S&4{q}upRkvEcij=F#qWT7NRC* z(1e7#{AlE)KtnEJn)@|#=fETZLSDDa*kz_uuucFRvBThiV&+cCo~w#7QM6!fHG!}) zf~d`zO4Y_{#M=Z+mHpGVVED=%XUYT-@RbNm0+y0ae)r2ZX&d|hPVN8EV;o4pb>%>$ z(0sH)3C`-biDzUX%)o+bl%9_*HjZQg$&k(jr_R#>PH9g)%sJe`Xe-0a#w6!uXbUBJ zU+&@T32u33TV`o9l;^@O$4>&Me(u!l2!&b>FJNN>*8HO?@9M&EM46m2G695iWj>w@ z%aSp$ZR*06DTSu{!dfxYkiyQny#wtD-&WvAEsJ*z;9Q|b@zYvB$p(hGLpkn(Fh;{2 zi7CHe0jQISovkge2Y#)2^ZzK-Z(o#ZkLY=%utFDja@^*vlT;pKb%({ThP|uX z)xztm8>ga~YVkI%fV+hiIW7AHwkm>3xnR1q>|&q5*&6heRBqnZ)~m zm$1O@)DlrXx~h%vaIn8rwiV{wk`BSnGaF@is01eWx5P6dw$M$8c0w(`-|a7}bTz=e z=t1pL*Y9|ln28?H>aN@Q^`a9 z2e(K?Jt#s-S-BK)6HiepjgSpx^b+EXaZ!CRVh%VMk@wMv#Jd%J{`;hgI$UT>(AW;o z?6uvrtG@X4&Ny{7UBNhENb6w%sR%3AqtrT&29~a18U66Ayh`2lvMZTqW@mTw;5eB$ zlm0+)`H!^$uV4cavF3iyMGOkI>(pv0`z8 zGf#_{{z9yT?Ozf5ekl3ttd`{pgKcRlbUIl8MKCwG)0;M2AV3RbT8OuUxqF*%oJ4ohKmqLbDrM;N*NJO#? zmERu%->=^NqH1DhLb8D)#(^iMqmy-#u6!imK9BrjZDn=!a`rou51xPrE0I$EUjiM+ zePm?h^%VDI8kg~K%wUV-Ucvn{*)ct6153&m%VE7-BkJVD%+Rt8IxwvKb6qay!Gl*K zgpX;-EwFKT?_3U8aTs(HAvH9%>0-9%XEa0uX>NW{uXGO!apk{gebKRi!RQ7j3!{Yy z)%EwMzh~%S^5LSm_*hd|XQhZjpb^$ub>7;XH}j|tqjz{V_Flnj0j@(+`a*spAKxZ; zi2wm2@@)L?CG$)*L|P=t6QKA)-A}^tDB}{66IUBpHLQWpo0Buv6|{YBEKJl#XjZ67 zwe?v=zch=b$6xkF5&JZ>ar1@iVfA?_kVj8o>x%7&fy}!YDkE8ViTGs0)&83i9uryA ztZ-#*g%QK+F{xshnDFUZUfCX6<|5hoXM3&vpR%pMiGAB~Qq^a|tW+_d@lL7CYT zS|>|nGl@tFZ@#iA|7qq)~ zDv7(jxjo!?g-?_a_~nhCJhJgZXdh?p(0pF)12ap@n543TxS-WP-t>eU^2FfevjcTL z!#AqV;rU(|Rh0M5tJGkmf0BAdN&u;X7=+R16}{DvPlhsLYG?ypNf5pjmGD_kTMqk( zOf#Xuut#=9*jlhm>+$j?31MYeXObuJo-H@D?CM`nFOp4go-YU~$%{;pjbTw*Tgrfk zM*D7k) zQ94u!{N5bB-_P`i{XxK@QjDIic7D=(WI{eXhPE|**E@-FpGaeB_eSH34&wqpdp^n; z&iXyUv<79VF%(N^|5m&I$M8S2xbtg&?R;8WdISUYdK7tblF56XE&3>5(NK#mZ#)B3 z>4&E66Zv`%m6v%k_PDD+CA-=c4a=DimWz{U z_`!vP$p)Y-?{v<6_n0!9+NBjK_odK<-Mx_x>mn@7#kJb%-%h$-K#Cd;2G|WZ29!(e z*RUW?_IN5AE8=*6U$qFLPB1PEyu4b%ypu-BOy&?zt zYDfwUetKE8#|Yph>iIrY3Z3_pqwS~rwI&h0|Hs3Sy--nJRn^2Dbst$G4T7RWjJ=&5 z8+iU6UxQqtNGM}C)k(`#1}qRkT}m!&6fRTwbIZ1Pp@TPUGjGQ&`LxoE@SWq`cBd%p zmc#Y{F0SOK<6(DKJ1k~Xl zc=y(Jw01(HE*}{`q~R*ceEte)ess8PZD2fY9!%piqkB2B6^F&o-P?d(fLX{iPLipv zJgIRAdKkAmvbOgjv6~V9jGB5$D84r8Tz$hN^(! zor|29A%SwNZaB3C49-;pZe823K?3bW1ZRpX#(cSJve70Xdrt-~F5YZ`excMKF=RcO z?m-t=!4k%f;LaEB&77_>!4HU~dg8nJwp>%ION)+NI<8q8j0CZ-dq*~utR;TlB3ye+ zC1N~kTFD$@MatJ6RZm9#wS*V}c49prb8XlL>u*QmL}CCUc{n>i#~*U|NnyD<$r0$} z0G81ekD?bPWT@#L{jJS#r}RU=Y7&i4Vp(mxi?1RWqd2zQq)@nR`H5B zEXwFgDHS%SgT)kgs>q1?-!7oK^oJI2w&91f zWkmBFPYMdzjK=XGtAnxRlJ_R-rHs0vp`i>W6I0W4$5HAvG+v4Ia21zd?~TBP`+y0& zbe;pqY`Oo2_-}$_Tygn$6Kv`>qna<7vy^M$zN1UkHuXt4!b&GI|4GVjF*c^9P0O(4Ipzzxq7S+Z)v z0Lgka)@HStmJwOQP5TM#A~1!4{2>MSAyb0rwz1I`XFKAl3|55q`myq7ixNK}<|#fr zxXu?676E(adr=J$Haz8T0ptzE2G*!AHsDke64+_ZdSDV@ViUKOI*53Q;W`q*VOO@3FsO$nTl3a=k#LGExiJG-Nh5 zqD)kN`AWVsl6i6wWU2?Eb;I_*78BDJPnw8|$pGJRphWMt1aL%}v=a?QL!CQXs36?( zQHfGELlV2!N4Lh}fxX~K^^bSzt1 zRikDoA6m3l5d^Q8senqLkqsTFz6Us7Vh#|P$D1RfBAN61xFr+@_5FO^>Q6Uy_fH5B z6djga!3a;|^Jz=(T#o9Bfch1K&ujw4&~YV%=l6D9@&SaU4X3V4`QO9i_l;y5NyK0C zj_ODlfp~=nA(K%Mrzr!b(Mu$n=EZ@k+DX>~h4$n$T8G2*A&;kz) zI_CNBZ@glD)zE;P=B9r_{(vBZGWg->@u877v?@SFKRROda4f?3z%{B|&*=q7hyc71 z*%sbtmqyDx%GFxEQ&rwK-~JQh?R(ooXJ;1t)L$}TDPcP&^GId*!IvA#fgP-bH9UN% z+&KDT#>()LJqF;~FHGg@E-$DO4Yl(~!L@J57Hdg~jNkebIq&s)mjCWSKMUHKB%nv-F z&gAMZ9!S9PcAWIV1&^@co%LhRPHH(s1d;vjC}ji9?#c6VogX-{G#DAVg4|f}(x0f* zd#!(Wh7f*Sg;L;E2KknDJRfF;kcBB&d;-bRgY!P7VZ2{iS$JO{s@S>%5E)IgJp~ru zxL1?*zNn<)F+o9|@sROo$;Pp~7~=S@Xut}a2Mry4=ZjH|GWhK?Coytla57iSalfE^ zeZ>E{8i#kG{4V$latHY0%e!!#Nt-U-o_D=Vey_Q|oRkXV!2YNBf&KE2rgt-NXf|I( zggP$XTIj?6BqKBMfRl$SWb zuZW325k?V|1xjR0-Cw|ldfEjuyiyS9fq+BAsfwD~F89`R3^6p99Q3tH5}EuOG&;H2 ze3P;dx_On8m0A)d`Vq(NAP^m)0|lNPi&QSSjm8m`~i`2T4)qN6tKU3Y_p5KSctgdUj`wgn8; zunkRiz@At@sTOp2(l$ZEq`oNPm)7WNfqMZF(-*veEvoUSpBEJz_bTc_=DN1o(Cg(k zT~kqQ%B6eP6=&y&;Gl8-Rlu(C3WqJ{D>s3A&obvfU5@j~(e=@Q^0kT<+vF+Ypb)L6oqzjo;!aSuiBuzCwG;4rfo%oO}V7l5r z4r836_cgnhL1R~+B&>g5Bj)Q_$0BlmNdk7;Jpo)3S^e0tHFYxS7<3x`Yinz|N6NgV za@Iwz%Plqt5X8ojqIs`h*!3$@v-!$2@GI&4gbJ3#Vr?CA<>db3v{(G88LmxPyc0+Y z6jJPycGrGNJN!KS$pKd<$4@F&5+6wcK%@o!DVDw(1#+_*MZX$c6G} z=i-#8i{%HQul={yYf=dro-(V=u(cV;D144@gf%uj`-Uex#QG6Dj!b9EKa|hSJ;=Mc z3B}DO^Yw*6X)@Bts6FW`v@@UQtP2Vo#zmxzF5yYEnw!Lpl$4q*pq672dVL|M*vyeR zprAN}I)Y?ZmuLq}RaKM22T12p%)5oZu|~cPZLl*wMz^lCI+r5Dk!y_k>ajh%ymPz$ zz&r9-^m$k{mnF-|$iNv1(WT&GR#gFXxTeuus%Ix%b`R+0?p&aNsrS6RUj1y^E*B?x(Es68cP zMn>K+-w{DeW$=T=lQNpj;5slYnx+fnB*ux8B12<`_sn3bN_ZTjtMdZB@n%3iXrkisGS1I5%OCE#B2D8e3xumDP9d5{!V^*nX5>+0m6)o|Z`L?r|`DdZWG# zP;x|Ko&=r$sOe)=_0xjWIuo-nu#`su#%c|v zu`#FAy#|q zw{aSasq&;&BjeCtAK*%p%yZtv%S0LJUb__N1=+&GMy)Ec3z%d!LgBH3lc7m%E0;~B z>ic5D%u;9GCnO}ql215+e_-l+BgcNcD(+ELUZqEr|7Jr&Kg#`lqx{!;L;wers)pw* z)dHJ?bWJ(CZl|~vf$i{-;XzD&n?~q4t&N|TopJOZgr8618;-;HLwKVn^8w(&$~cEE zB>ZF6wT^pY#(jHHz1ZFJl$MmwPmIsdS++ofnyCs+H?*Q3C7m=Eecnn+38SqNNXfZEJ=giVp`~fc)+p}GxbbNKT?7~h{vyN~0 z>vd-`~S^uV+;*4YGJ;KtM0ruR@5cKs<3#V$akxtZ|6gs*! zt>E1kGoHs@go!PM;9CUTh;Y(CCBdppffSZ}&a*Cu2XfklO#+Qol6!4gLpXsiT$4VPF)>Z`lsAnKq^zCWGn z7a#M5L%(dKD9Y0mT)`*7!fWfvDospGu*~(Lml64%@`wf2YCd29N7JZhOyCl(Xl*KH zd~yUJK+B;a;&VJ@0{rgMU8p@0x|qFMl#+0pa*3bY9295mBzWO6Bdsg5b|X46qHDy% z`hr2?ey52;PRsoN`lw5@jBa>G2!#euem~z}^NVodMTbn8|EKZ1e`!4LSnDi6+XOGB zlWfO3}$ zV>+BAn^sT{Q}-+p9*EMa&kta-qegg}3fU7EXgh;~S-vOXdO%EQ7`RhNEpp;xt=6cu9a!~32+DoG2N7m&>j@JKtS*D^tV5xn_x|ijk4ak%kENY#}jmVj& zIgR~rRpokIEH8Xe%>bLEuu53Uw$=JG2yE*e^MO~LC_WlmYxDU-i9FpvGrFAa2+uQx z`?+_wLhyIFqw+lsVd3%D%>}7W<|DDa&i3Pu_uV*gQjQ`f<^XitEDzGD> z$^|obGpN7`%=VUL0J8)u5>@mCLl=)rQP^^9U1G9XlG$`e=EM#U_2@X7GG`M3X%k5MD+s%aY&L|%~thbs?kdM{C?{-y2RQzmI z@}QI(@>-u)ni;!zhzMi~{bifR6;SDf9Gzi?il&8P&RjY=i!jXb{>H8!k`Cv}n(qck zNQN1#U3t*az}}di^flMx?6(PV+)5xOB*I{;mctn~&OEomqYj14V(A1XCm-BdUtJW( zm0$y>z2@4`$<{r7iwO(}h-kQN)9kPfK_2>$-Le6E^tH5bqs1on=HPC7ySwoch=}$0 zAf%v5upOaYnDyg{Rus2g!Ffe#5^5`7)|M30mfOJfFO+S3o2p4<14l%SgbJ8AnQ^&& zB>Yv2$QKjMdgG9K(XzKc20UF_=1XIIju+0m{0^RFe)3!+`d)znM;tS~!EheGmOR7y z>kxZaK&D;WDAZp_^{JIg_~g+fS_R9hj^-eC5{lFeO-$4F}92CjW28>;mHol$_M%)3KwXv94aCI5$mh!g$ zr*9#C>05+&wzS_tKsxbw(2@e-EZbUJpaTpBOf(rP?E0kPf?qb03$YZB$&(;qfRJVW zp#&)(ogZ)zZYqG5fiRLSl#%BM&1N$`Fm(xn$gbP&b$vzaki@x^<=rm`jgq|ET!Ho zkDH&|ez0N2^%B^~Fji`Iron#zAhL9Udvg-r_o*>ctl(F&xVZ3WQJ* zL=6_e|Mm;D-Thw19M&KP3pf06@BG4HTCAy7Vmhc50ripKFYQEerx(2RJD%+z3n_~9 z;bx7JgAeLN7}E^(bw2jw0&YNU4O37M%@Jw)&B`G&%LrJNXaJqym zNTIw&j0)5-0ZAmM8f9ufeMJ?ss3%iq_}Qh3pNN80j;FH=LO+uq`Hv0{d?h09;1R9& z3R*c}m+ML3I4~coCYJXjjRGn%yIpU;11&vM4C_g63kD?a_N7a$?vYSVdg{!Kjzr{4 z$o{e-{M9LXAu&~Nz)>qmlNdZ*Qa((g5;#kvaH~m#U@Ax1(UP(^at2DI6nX`n@;2(jvduz_`{eJ^e$0@a zcmMP+wWlIak~)g+{&hdPlI)M4N$kA+j1`BHz9iX81dbn|OeQ65`dm9Ii4x}_VUdBy z_*x?F}*72hpLw!{z+?$?-ag=j8_nwRaNfSkyTH zFI;eAVT9236Vylp8FnwMjj^IxH_Rg--0-AErOI|}SXTOsF@Gf{7!`O<+YYd&TnGtQ zVw0EeJ$pjkaPqJyB{D>f!*r8fqc{^W$s%XEP?KDRf1?2)-?_m1Gu(}zHsdX;zsEww zlPD16B&JHj6~{o0C=C=zsAY<36PZc)pohni9hs`?7Q7#e$~cn^2SPOhFC49h25(P$ zvBp4nEyK7G>I%T%tH|;z-rB?$Ox470^bItz>wsL;0hw+;%U5T(@wabrPL_NJUtJC! zW|h%wp_zF2(LpCekTm|Ri$o?ff~9&5uimE;z%gqsMTb&!EupLUVxS;ad~H#RFt%Jz zygB^9#96kga+4Mu4HPWWe~t={E~lc2SU|wHW)Kj7A^!9#{@r2g9F#(|A#Z3g@L|O5 z?N-&a#c`8W=W_soZY?)9Jo&b^z!$m+)R9>52G`{&Q$Y-V0DLr2VJkSH%6o&c6g<;< zt+2F*X<@0<;zBINO|W@+-jx1ETR$B1T9%9&Zk5 z%-gIof*Fo#@xO@La-1?$WkFhu&NcLc>S`HEmc%;6YIl(}LQ01g4hN zX^J+zKPN$Kc#eDL&MFz*t?+-qzFt*k^8I=w%p;O%;BMd3=(Pz8#lvK^glVz+%Qd-g z@o%FwW4;a}UxL6LCWOR8Dgodqg074iJ_wK-*&U&s8WW-!2om$g7O_ZqQ4&!|3S&2F zw{u^KeIgCp2J9$^wxmi3wh1@5&F=6gNNUI=lUc>dp^?!fG?r&RNvIN|0RTTW8-g+U zAn$PX}0FW7mn^V(C>x9A6!(m>i)^*qH$78O;)UA< za%Ygx@ERW7(;Ggwol>gfEK$XXtTS#y*o%bKb>#59Fqg$*EA(qaq`|USv`-7rJ zC5ltNg}6*qnJgVKFmcd?a01N_+#b&G3ZF$>g0TYY%xNpyt&qY+)4V4UGKrN@*<3(s zyPZ|;#rQ#Q7DP{~Y)Tud0PQfszN76fUW`$YK4+9eHFSJc33Ow5z?uKIbEypWp3#oQ1 zI89e{(?UwIPVbIr-FHu>qX%W3X&q{RgjBajU=5By>)=OzYU@--1Y{^kn`A3S-Q zZ)1CV$>OPc?ql^n9HmdwjSsgxkb43ZUpAsWM&WK9 zXG%>x8)l(ZSt###N$}lWKgcaz8E8Q9z@=wjsa8EF<3-j{`VWfM?P6Ehp&y>zVhsfP z4>cbgrmjmoMMwAlqv;w0oo&Vbt@DX}OnzIqV@T@aG*Ln{PczqE zA2PDzb))7NFE>Nwc^L1L=Purs+31{asv*iHX8SLEt4{;-ud4Rwdx8A)qd3b0_KkbU z`wdqMo$)~hWCHWz0dkTM^;rD35DR^$s!orHLS$Y&TFsA z0QyCwWrdsIBXRmsrp9E^Lhi9uql(cH89VkeiI(X|?7eX|k-3uQ#YJGwWIoHmKQVtz z7}-l|f}3PufmXm)%_I~HH^E0d_TlujQ90iT9^z3Pq#|$J>wbfp9B&IMmfEC7fv?+s z2jqCICDLX+3m^ftG5Q^T;3;SSFAG3nw_4&AbN3q-2!vgCz!e*R9K&j}^qC2B^^)v~ zl+0rXGR(QI@+Km%;R#VDz8)M!Y&{||o8nWbjJQazv~bz|TAh6IRUwsnIUp8wPUN5H zXZb~Lfkx)9Q`9%=(7*%ES>fhiZd&dFErH*p{~=e%;NH~`kzd8rTieGL#4y&~RG5%; zC)^xkYh>oiA4e9=dr`jSVwyVG{do)?oq|t&9J;J6?9f^YGeh-Jp;t9$K(;i1dWv@y zlFtDD%E}ugGvh1!w>LWIMa2tV-+I)nYU-x~^J+?(ya7@P{R(p7b{h0=w!HwLTuC~T z3^V&I(TXf&Rd%7@E=d^^Mh|oyi?AF_wB#2A@gPiF6j>}mQ2&$>@tuiHSyuz5|5L-R zbaWL)@&O6({BZV~8^1~)O@X0kHQgEY!L8EUn1X=#s;7M zai1kD=97Loy53^uz*BlCm~Kf72+!&dQjIV)<@y?D)=sb~#DO&5zKhhPDw8|UE}p&A zy37t*CquGaNMaUYO0~nMCBhbwksCuZ+gwT)m zlf4V6rX3A38z8)%s>jAnbi!AEq8snVa|^lq7z)dvjU@@7R?m0@rf`u=)R|f#Y0v0L@9f4` zWvKIq(A_-_9+sAPm|T+Q<}o5ZuDHVHAHXvH(vemd=o^$26Wg^<)Hy8KciIcDL}^fsxr1=yGF2QLFEJ2^p5*}0nK*F=-9Gq5lP=f8i& zAU{eZi}_75N#`W;N0mbOp~?KWclnXMY2WjKZTqaL^DLh?6AeKj-s53kJ+)|9by1E2 zEvuOeF*PV|0ESl5 z{-JEW@6Chs+=akIN&Mi^)C4~>==E3tdG1TmRhpIg!KRaGTE%fit@gZ&_OaGJVsP3( z0pgTBzD|1tE5HJ>xw&Toqs#3ZK7MT|M=mJf>3VpzL+SFpyBe%-|LuMGzO{U3-%#_? zb#`Yjei4HLGZ?8rn-R!o>=`oXF4bI42P1_H6;2nK%4hvKT?_haTu!f_ZZ!Weo?Nd!-9YN2QVgwk6mIa{qj2|^zl%zWbVou@%JCOaCLIzPfzR)7m+$i^ zUrdLu`tpuiZ`Ks;AjRdZ`A`)&q1&NCb)4(kKY>Ms5X@u~E(#QC&;Y2Y0*t+#FE`(Rv4uIXWd&kEUX~opM%CFa~;?o9*ckCs#dSaOE*6 zx?<^@5fp1dc{pqLLSCV3q#;*eEiKGTu%DCpAAE)KUk;KYcxs%osfa{RH@K{+Kmrk*lj|0A7rGUxDf7S2&GzYj^OYqTyo z7G|=zM-eW4-5*sk>)4Pv7`CX48revQR@EH4#c&{xbpXmDK8$A9M&3R5Fk9Q z(t}BqGCJ7e=W-JDldl)Un;bSZT^ocEryPA!J=2x$fzp0XS@fh)GLC#UT!@LfQ0BOG zLT?u*J-#NUvrVJ(lZDBDBI-s^Fh0t%sxI6)D8W8~!vWs1wOK;`@u)=m+iZz9pmqpI z#wlFIXgDe!G-PM}QW4aQVm^<_#*OB7p1Ty_iNAu6n#M~u zva3-IlNW6Yus6weP#{iw;Yi2C==+v$&w(~Dru9S${c|yOXpQ$3*iafBkrVEpMN>5fszH_Ev z9ekE3vmy`l!;k<}u>sr3otp9|{<7m`dGX0Prx9+<`-@D%)UY7l1E{Ee@?BjKcQ}e> z7GanbP3Z2REru(9bKnKGKW9n+4Jk5g>OJ{MIC!&4NmiX_M6D=#c z;b0-_gi+_T6bG-Qv>?XnoD`_FWxu(YG#;p4+jtJ`N?xC%D1Bhgw3uWuzUO947)l82pCzv-W9%(9Me;8e#Z_w7UA9_DT7G2Rj2H_x7J zee1oE2H;S*FoJs_D04NR;4u^y7qmnF?U|cQ^g#gPNk8VBOwDN4r<{{ zaFNhc*4O0+m~guW)xFfAgD(-X1ivhih2X$V>_r96#C^x2K+xd9q~v5tEqc|5b-D*{ z8mc=iXh3M`Bp)$>l!vJkWglrIznLRMW(Dc{sN$+(KESM?%Y=reXvVNr1N&6()zFcw z7t)uM+m#IyFDpNW#vFTqhWqah9*yr;{y^Anew;7GwlIxy9wpqx2zMh^hB$o+i8#L+3r@dqe8Q@D~6fkTd=sz&Q zPs}h3BaGMqUDcwTIJno3O2j`rS;FCKBwZch)kCv=^^W>CIjf*Li_GL$h5@5I7SUK8 zX#VrD2CMr7X+=?@8WWi>NEg*Iq^J59)DT4o_l^5q#i6y^_W;FMpb-zB2NS8hZg!vK z{F+Lp)o=qj&3=EpuB^=YibpLZs=f=S;EDoiJ&U$v6eLf~U5ENj^f^JW%Up()-hJTT$tF!wyFS zQf!vlP&*NNPLr*W4R<{39Bb_0&Fa#-vy=da^*>zX2L_A|!x$<=!5na1rydj-7*PRf zABZ*&O?`u`9`1UXffsfxjz!x6ViS~P8BveiO#qfIXp;!}Ic(XyE_cBOCM_Ftsc-2t ztC3}4Qt?;4>x-?A&MEVe^a0+|xLM*R2rp@AZ4u^*a~RxznqmoD_JX@M9o8ah-IRrX zkw9*EMe)+na;)DT)pqVGPZsh^P9#up?++Kc*j{*NAIdlB0o-D953Gscrl2z*Wau_UwV-D)8XTbGnP<||X~J2pu`Yyj@$}Hk zu)AuK5P4{)SDI<9DTx-!0+kRbn=+5BdBI?&%6GuM8W?#NT?uC zQ|5AlS+CMZDlEudM6M}&1=CcS1it=34UN= zbjRn)nfxtrD5LBkd@bIq8gN=;N`-u#Z5Mn7F{)*Wq1)U!rNZZFh^lnX8mJR#C+^NB za2T~4&+hu|uUOmj{IpPF!ao%3>Zme2U-AV#`Ow@h7G*@TqlBs@k{v!#Yu|XQ3ngQpr|Pv{44mL^#KSHNR64ZFsR3c);D*GM`)B zu&z=#Y(43j&ae8cMx(WQi@yE1)jWS+a@WL1#wyG8I|l}J|W1$jd3ajJT73@y8Qdztl<6ikyq=MyxzG1)ZMQNj-TJuQ5Jimz_B4RPj%8bzR49)4D8*zw_+ZsPLEv=A3M)b4P zE4pm8dH%c$2V$QYdd*;RjgqM79CxMg9RnAJW}(in6V9K^ZkH?rOHw@587ZQ)Hax(< z3r>FE;Ie3|*i`yzoP<75)0W@ud*e0-S5E>J#RqI(DZKM-wWzv0Jz8^1ZhkJY$u08L z0zccElQScpucr_}qr8{U)G&1_Veuo+#6$lijJj}FS?pRZm*tfmpBR=)4lICzoF)E> zMM3?O4|DyWLuZD(fK^`ldL=7ipD9{?O67OMrk|a0S*0`BS^VF;KqJCq_a#nM3rSAE zmHU<)>_vHj6yD^){cB9Jcdt(8pQM*(D|5*&KJLI}8Tt1v=DMxxtw+K7?pNM##Gnyn zPNA}rl&u-jOYXZLBkS4L@B2+0v*4erv9pMe9Z^Oel*~SU=>PPpf4-}xeU&S4U|(2x zHj485>?1|@$m-98bPo3<<^x2OKBm|ZwV{Td^7oEC&%Dhve>)r}F*R@8=iiik+TCZ$%#}JCvBx zvjc5J1HZ#SLDbW<87`foaz1)Axo~%(oO29%+~D}_2|BwP*3@PG8|$6KfCIIek<-#A z%e#x+qdv*LZH2wv0vE!089!>8#H0A<(kF!*oeM#1_3O0Sy2dKv&>@ePmyiMe~2PVG)V?|SeS-gk5WJ{5J$>K>XN8pVJKoe=yHb*@>+1- z)(l)|6OTJ3Gj;<3AZ=cYn@>-*Zfs&GSvqmtuw#N{rH}Uhb0SC;zYSz){LLDvr4GaI zV62)`LeDvcTBw`zRS5)s_OE(aEN1?fj2rzoNRD%#SqMX?5_!)0fD~-pSLzSALFw#j zAkE<~fw3~D-I$D1iPsbZ(BWVHsScho?M2V79?m+_50OoRY;+0QEig3Wq#lq;h@#W_ z`pJs8qHpqH6uGfi;#a6cFogylDwv{jtJ4m&o1pN}xq zLrngSN9>s*ScTsKD5BPV!+6Dep*&N=XBk9S$LTcZHqE;n&O$LRUPz}I&MI;EQgsa^ zs991$s9CJ3k46@+)(WycArgGSA1L&hS!o9u=y zRkW2Ct{~@6YQDEAj2e9Wkx^2=Sfji#e*Z#pzwe~5B_HPu8fgloJG?raB*+&{iFyX*e&s$XY4^M zchy{W<#=3xrcdr))TL`OS}%U;U%n5vk9cjr68$vT9(R55`svl#e7E0q<|t~dUP9)l zTvOPLz+G+Ka8He8N{MI(Hmf8{{r1ec~I8Ns2gC3TC#(TA2aZ>%JK?`8q3czc2|>0GajBQQVLozuV%Zca5E1h}rb z;*GmNR(=);bL+E)N@$X-^RQ#eUV1&T>3kCBhQAB5v#l-r3_X648&+tI zT9HY9I*T!r}}krN&OCdLN_q6GP&|~zkdom48gDd+!r9r4P!!T={Uq0B~CFG znwZU5S)nA=RRhnHB@9>L&NHB_Mya^=855^O(x^5>hK7<8->P7CRE86!<21s>p-NlR z%&R8Eo8%|q_s(P8)r7~KFISN*t{hM3&hQbEATBt3lG4`JD|*+h z!s_+=vNz;j*I3t?uxMhCcYGwvV(N&(j<&-un9^Q$eBaj{2(q;W|Lr)s!u7~I0I#*Z zOTM;b8C=7x0KLf60QuGiY|cn1@&tPstPu`1`xZVo2j$mj=!nO*>!S&zQpaPdj+yAq zDZ!^DNl#8Y_Si}p1KLx`EcO5Y#?D29zp(}HXgQy5=edwk=+Rl4pePSkrW`F4g&&JX z4X(v8ZYifA)5Q{H#G0z?&&#a}a>NKua9(KfX$s0&!$oe$q&V0IFxnyJKfI8ksHuc= zazZ7Y1CGOOC2LC}E`l&4S&ej7qyY*)a!=qCrQ^$FdF3DqA3Z?i1%hqdME0JF8}5K| zQtX5TkKyCe;;2~Lj&c-qm;)uyg^$@OpG^3doq|oaNJhh;%8kwR9ucilxTb77g?fu! zzl5hs5O*0%56+b9*N$y=4o~X?zVKL6rHM{BvuICmA1H6M%GPUiMz-qD@CF|)xSc-M z$S?G56Ag3{M$UFT_MShWPrG?S3%Sz@toovhIThaD@iy3LNRKp9jVKX+eAmM0fP&Qf z5*MBGi#*^&iZh|Z$BlL#tcLo>&VyM-z(1C;KjTNUlI(#CW~aIgoJ22XJ%t!JcjVKn znsoTWS)p6*0QN6$T+^YY{TM@CRP3^*c9qlfzEl#R98ZwSzHI;YgZs|=I&FSo>Y>!) z3_H1acC%Q9!n(bDxJCmu{(NHVF|FWWib+vRtB)TLu9|56U8eyyX2_UC7YWfnc%(8N z!6Yyh!fA&mfUI6L1p)(p zwi&1yr$Dw_<&3MFtXs$s*qPbH45)l87uNQiE0|l`w6Q`^bt&v_G_I#Q_pOLJQ zQ?p;PZr`|!v<58N=k2AS65-0>efKYvW6hYM-uwjm^nr&Ivf$ymI)nhpkNBU`p9-q+ z8a2d}hqWxsg88PtPQuuJLNPiqgeg*pPs1rpXOby7q?8!AJ2;?FOzjjPGuPaN<~`W1 z4hz_rG*XvYKkg)%6T5m*Not2^JJjCb*;{QQ{mMEFs`GRsIkya)t0VN{UrLBg_e9_S zyLTx0OxLU|t?PjP*&D4&POgSv{8Cnr2AfsIu!Y8=@9tlYQji^Uo;PBL-Djq z_Q@bSaTsf`pwPBFM#!z3f_JBg9QTITo8)tvXwa=aI!lN()ejbX{UUJfM&PSG@3-8; z7lHO3xl^PN<3h0L7DSP2W%>M`J*m<+kT(hry_3NW?v;`HhZwvc7gm%BA%D6WbhrC{ z?3qu_H0;=DrK)T;5dyyRDaM|!ER3p@X>xFEnVWdW1u}j(s()fYyV|YhuH2Z5lrJZ_ zb9OM1{)u98b920nF?hJ7wHw`7_nTEsv8|ToNL9eaNrMm7ipJbAWTlp0WqFL?o{cB+ z5F(ftT`OMK3hNgt%R7;nOCqj?Hh_p;mS!pCs=ORU8YbS-9IuF~K}BsWSAfF{vw7+3 z^(_Cr<>4;5zGmdFz4w*XYv!>4Uj7Jfe<7yMn%i4@>5PehrVirSgNEIgHYFOgbFql^ zn5b8UuborkcFE8UBZusj-+{MOSA?bu1@mmMSi55q4SWC;H3@rwaxdeVb!|&A>GIU1 zWCKr7he5@F^5MZwsFo%`fkU7Pi)cjtos&aY?ivGbgF-<1U$$?91;Gd51n~WLVnt=cnagnp7f=K9o9Z z?6rw`^nxzhUB+&RZogn8#0t&7G99CZ)`j|@#(cNxD@gfmq>~g=(RJr~WoyoTOr$W~ zisUasIMk0ovP40}>N?jr7Wjwaog)7NOvG?BTB}4v%ZPc#So}5!?CG)_Uf!x4>d_k=6+fh#it{t?!PJbvJc^kS)kbTeQZGdh z+PiyjkqL+nX#Q1x>5$PsJeX6`#uzp{4e`s;ZF%KlB!cl_Z;YR2V0 zZ*MGYF=D_6xMj7DT5|aI^y3r^a7L*dRIhETEZhf|Lz8@%e^hi$C~9FV$mO`zd$jk&?lR~Gj;}8 z?Hlq;gWhp=dLn#=Nx(6pufv@yeU=reSD%VpF{5FFaN~4ggRzOsfjxB0U5#Tv^52d2 zL-?&0L|yr{4hmn-0kXQ|4={B~fxrV!!N*h}f9~q_-O|!>79r5K3MGT7hC@^@I+pO%y5 z&8|^?IbFL6F=n%U@pnvcS$)~P{ZseLjamO0Ji4bJ~M;P@4I|B0! z>R0<|CWbtYIjU{-qlaHwZkSuHVr(nj$Tw;6j^N}O zD# ze%fZ-3Jjbk;v)vLn3mLtV@ohoynL~^*4hJLi6a%o;M8DjV_s^gF25Wp2Vv5v$Opb5 zC&Npo1UOe^Cj9T?4djYlq^<<2h;}nJz5=r;?^Y`fg~aYUoe*Rr1-7bge2Eq1U!yhy z?{5cHsr5soA-|u)QU)~8E>SO}!Xobs;`a)Q8HFD5Nc~S00kgs|F1|3=lC6I~Ua$Ab z|9+ik2%|B2f4Qe)Vv?l8`IVU-OpTC-0r^eyh3g`{askC*?iCYNOkF)e%J3jnYE%{v z2?(^3+SU3Xr#f=>*Fg+I-Q+H}a0HCNS!NOad(pXPfTH5)`YzUKn&T6gSQ9E-F zIXR{TEPYT>ahXpR2>78zx=k#Dq1H%kPkEK0K1r!$EPk6oH_qHwj1q!)~-&=)M}=y)h0%uv|m?o-rz zJIn+DXpT7Agaf*C)`jDWkCzBPgl%JMm*ZK$+f}IWAlR}V5+9G%*wiH2h#*G9&OxZk z+$Svh27||}7X~&ukUP38346U?JWAZV#Lq4c`N1$T5wXRYsNFeFO-lhZf9=;D^YW?Ub&r6e4*Tzgr8e+>)s1r(7fFheKl z(N+k@iW$F*)ZH@;gWiA85gB@64)@K77h5qnO=7oh56DOfD8(Bdu-j>s#!;IFk#Mc;N0D%^J&=NERbXl$|tM ze9~+u#zifGBF_4SS#4S30?N>cELf@WX{-O^09Rp9vo}e5tn_m?2K^fwR_NERL9Mhq z!6s5>gu%Cde_jPexhwcnIWsjkca7MNqJ=-Sv-l(Uy+1OAMMUUv#rsZne~zwol*9OI zV<;3f_el+H?Sk+wWdHHrl1KUySuv(mF`Qj4Y05H$|M=HrEcM+<{gmRq&8jin@BiGyQ0J_krqcR+?lOevW_6JJ^~2_x+K}i3030 zSHs1ousTb?(f*8av+KaiYSa%i<*!Xi3VIM*OTMj7l5i0FGAJxcQX8BZmTc6>#O;ms zd9i5#|DRI#vP+eUm>Py?6B0)krIfDL(BzgIDHs4GjtH!;R(zr+J-gdWc!&5Ik6CK- zDkUP585?9PPdLoWVpa`d-mQcP5E;2=y`^dm@PR21{qUPl6EqcZw*ippX@t5R3bj6- zo}ffbY(S%!mHTYG4r~3>3&29X94V8e98G6Mfa?$Tv%UWGA+vPmy5^*$gn~=FEW4qc z4A&a1(s!68M3>3HWy|8&SNHGAKzjhSn#2=6ljwIS%2y}7=?HEKxl7=h=)Lqm)W`cX zYn?SSk|OJ0}VLDUB&W z!FZ()Weu#QiTN65LT|wwWs6f1J(A93a& zzp}TAl}sC>u|AGRu+L~RHNS(U(s=(Kz(tiRG_fU25S&CRI8I>D_ZBNX(N-wvRElxf zY`Vy#T$u_5db7m(dnR3aG=JpmRf{bxWYh+J}?p!LJZg{ZWs zR1qRcU~p6k_#Wk=Tp%!FZCi|m)RMlB%;0_5**bb;t32^O(1~d4KVN3QK@;IA2N{L< z1qm;Hu;nKmG=b^&zNH8E+U|SUM(L#l7~ZR;Vl-Y`b3N8PKgM`R4l42-T>5ZIlm_DR zkOBN&oQyjGf}lOSeb!4C6%ZY?#sH&43Vu11NI6`G=tM*`!#`En ztA@b(wwiJKJoTHyh^4h2zUkH#{6y5-$WgIzC}!2k$tS2h7mjQSn)d$8bTkmY+e==P5<`AKtK>*M!NNahhN6#mytzg#VO7{-v>6z zd%9RAwLXh@cH^?xl@9sa6W5dA1Nwbs1;-Fz^9ghv^QXU=+^E{_iQ6Zt-xPii(KLf1V{8l25?Fe70(j9{Y4cV2~JnSPcQ}!c3?1H{y!= z+FXV<(D_;{IJpdp??JPd*!3iK@uw7|~PM&}SI-K!s7vK4N8 z(r4~{pD!oHDQ6L)Nl_}(kFkZnm?UQCK>@xD!YD^|)t|l|kxEfw0M^cb*W$5F&eaZ( z5FMbgfcNEQuL-DQ>|~Y_{6vMRoCuX5%bj z2#oQ20?Cx>8k8a@G#m|VH9-8ml|xRa0w^%Z4%G+=!zlroA{y#9bcDVI2*<*#fNClEph5_ccuNcX zZ$7sTjvAatp%!w$45z|$J5Cazx)B?}E@}sc*-cJ18R+-SC=kb7dFHch^{>BU(i&;p zW6+<%M$v17s!=6>vJz9KCi4(KCnH}6`jQOxIiPHRoWtmGb;P!@?@@~)1o*H>zloR8 z{9}smh#{Y=W6!7f`1rtad?9XG1G~qM_a*A8-8Lw2FZFc>tL2y3^P;Ny9C{csYYSt_ z-~&APRig*wT9MIp^t9KJaaEBWWT4WyQ19mxVg{kC&ytlEH8f;6zR^!g<>T0*E^wz3 z7;C`zfYMxUVzM;lBWkHtlT&B>t@@`Q$VmYD@x*;HpHc5<%j9}6sxHb(zjN&^l71Uh zpz%Jt0S%tk4%ACwD4i^2HzoZBabC$X^1wZ1J{%zm+T^KWlZzsZx|;{Ng$jYQQc+T| zk60aRb<)y*#)g}Ml2h)O-er5nH>_fNHK)*k|8%Fo!ScYlFh2whu%M7;co!YAidi`M z`*$%5cZ>$4NS6RK*0L00!X;DQOdpqE_W@jM3#AYUUtR*uq_K_Y^gfbLsM13mY)Nbm zk{iA4Wp}={x%`u+Mr=TJTDI|cIP15X*swXBAOaFa7wOR3Qe?>o5J66N7}}AMZF%%= z@sag)lqW2&uQ9XYVC5kGTO1yC{apC?&ZInMa9^KB(~m<;41ayiM@h?+$94+gRH_7@ zJIeQe`oFS&%!k@#CedT92*ohDAI=8_L2HmAD#w~2=6U5MjU&Q<#6@488~QoD>Lhba z-^o=z#RU5I8KK6fQ>$T|1N0PxcS@89!-?LCyYDhIm>3D^pc8zmDb6&zp;&7t%GFNv zjJ0G-3BVe->%4!giRMKrvYGNwQjPi~E7bwf3QH`}f;156sOV|xm*&E2zKj`8!R_#G zksV5ET3r291x2J}mykwG4|uo^KI*hT)Gn_MirXy>y)!a9J1X#CAnhygk6 zg0G1%FB>n&%e#`iNZp>GT7)OSmT?jzM^=`mkf?svRu7Gwiox?IDZ(&;K4_3)@v9$- zGvbs(faT5Q8w>|Qy8NeQiH(EP!L^vyF!C5fYASgS+&Osv4}f6RU_JykyHJEzcBNDk zPY}627j9d;K-9g!$5Z&GDpQnR)p!{t1>LaX)YT+k?DG7 zcJ_|TyH1OWc=g{is=v%3dS?}-=oTBob)dP4CqYp=pzW8TWUCt;W35oSc=E?%XGnbkH!bc6j_^S&Ss)6;44Cad`4)jDrz$ya#9CxaQVwS9bP4 zaozz{u0o8|hu{|ufY-9}>Dd*$AKMl@4@i*_S0~ zb)dWqW!SANmaf-n3Pl^)*|5qmGK;?`jJJh>emT|t@gqKQV@&?zKs9CEHyH56Wqzcf zoFWSJhdpM682xr#*f@mNJwS7VE+AB5x3opUTk-CgbI<*J@YPAPN9puTGWulsO!qyr zd-3dSdE&bJ^r0o&r)$e+PxDgzyZrRY$lZ^Zw!ylyBQVuwvan5Sk9{`&I_PsJgmrd& z6tgUKd3|hmy!@%Ts7D!GX8O_{J;4QX{m}w={gLy+*Y*9~W3PKFdEw&yThF)VmnP82 z-3v&!tJ+7QhUHc5M@zx-Syxg!X!-c)w}&y`oMRd7`^m-e@|?9Ae;)br*|Ev|`a;W# zYuDM?$o<9Gnyjs|Z_Y~b*7S+({lkII`_IcNAEoA(ulJ9%z-6zqL&yXGZrlb&zQ_j{B2#SyB@ zqGMN6M#;;TM@``pjXm;r;Og==ZF62&Ky)G)HHJVyUR%ZHzjCn>gh){|qO<>X@hq@( zgjJnC9Dqz`e1GYJe)`>S{5Ef1*h_gh*8QQH zLN+527e=lMG+k!}$s_@cPQT&9E66&5of-`6Ngwzo%hJHqJ}IFghyNPbYrp4m=hX&Mz~U zY1!{5k^3&ygp;0VE!QZAdPNtasWoqwGJK20MIWV%B}n~gP`SeU0;*ZqS@N0lBWLvU z1rpv1UctQ-RJj?WqtdEQ93^W- zqtX`CYd<>h?CC!1=*dH*ud8C30o(H`khmT`i>V!2ZO90cU;tBcKnVO?s;|)Qt!e4V z0OXgOxLLS4P86RwPgyr>$L{H-BO~tz1rl4N0r~m7{7j_t%dNP{)q@g3>|*I%kH^jjW%e7%G=&qCy1*`xci#eI*~v;+w=66b%?>}jFDZRhpc z(!Crbr^P%nZckj;<9kC2tD)fC5uDR@(aK`7a7BvpR&s;4ba0dOjxVCG_xaDMTHleE z`K9Yql1q=jKZKfepf{IK8kZlr!s#;Cvc5FoLP1CSRqhd?%6_fyh9rI<>DM(Ni72l} ztE~1y?k<%O4mb8*!>P{a7;uhBn~O%m&DN&vaL7w<0JRtM5oMQv|#rymr~qEm)V z8!|l(!GQV=CY1@r;=9>`7`aff-o;o7t>z<`<^m-dsuw#v@H}cA~UeCYFIg3HvEl zq5&DBZWu)Mw&f-<4%F*chU?*R!ciHnz3N~k1v>sw3EOp5(`2BL@9xt{6yR55>02%~p~Q&UXVR>rT3 zJgCQ;+=;VV7c<v*NlUQdrA(>=3Uu7*n?6#bxJ7^S;FLSOCin#&88|*w@MR0hS zHv=vwnaqe)1$^Fd6c}d$cqr@`;vMok*iw&B6cCJ!X+#UgO6$kaXlBdpf_pIl6x$E= zvkNDWfFb*!#nHh;nmL-x(ktSeHlF}a)T#Pwul3!X<@YfkTBU_-Adea*7{O58PnS8y z0Ac7-jCAdI$UNMIg|rR_^R>sq2yJ+0Fl5ET4DR3#RwC#E@C{FZ=%+dxsXEh1lt^ys z5ezDsV=VWvghq{q(nn{J@n3@2M@XtE*(w^A!}c=WKG*z3mK8%tYqlNCmy*If%~i>n zY)*TM5~lvS{t-c_nwpMio@9Q(dFVjrq}hjWbC{%nfj?z-SBw)+gsrHhaEwIFq3epZ zxG1&Y4t`6V37lzhjDWHtb`kJXE=y=nvVNnm#mQ#5iG}{C2_$s1zd7zzBBbbob(p|j zX5Z&MeASK`xM?ACr}%F}u$Ns?XppiBbm5 zZBF@fNO`91%9Od{1TQO;Gk}S0yj2B{p zItK|vugX>`1bW+UM;tD>ARVLdtleU^izyG7TcS7!V9IlTSrFG|iIx_7fX7Tg6NTQ-&JpF`jOLKA*MJqhc5rN*v_pUDMpA zXL;<_XIR+FBC#7J*@$?jh~P;Oza9{OUWvV=Nh{3*HGXI5vTeZCff?lIo%R}VrzIoX zZ()H?A#$st9DCrW)`YOC`r-K6f9wG7IQ<3Np0U;@eTT5fttams}N{!K~CcB*72->KXrftQJA*}M)y>`_AM5_@u}~w^Q4xI(<)ssfHQ-9ObVR~ znR58-Q(AfuhSStg?ci`h13$F#cdKE+ORgRH`3N=(1#X&X9?E(9;h^23m#2lYWu&#HWBfe!N9m~h|> z*L)!xV^BoTO7#p>&Q&xVL6L~VL3PRqLg`S{?VS_%LZ7Yu!a;=qCMBe1r_{tr-w|Xq z^#HJB!gg}cTYYzvH(AV*+^~0wGR*OooSgG{hoU|(hcPpK8;*SGxgU?{HrL}XL@sR& zMhKaKq*Z909R+wofrP@{ZX^#}t_8LVcs@V1xk*E)ERU}WKnEtiCNP^H6qV=m6v8x) zp1Te`=V{Zk`hD*UQq%>0ZKz~zRy4y%s(_MMjAE3Y8G^UHdxu1LOZ?jy%TF$S;#)S@ zbIRpa|4B^{!oDF)K@T!zi5Y_z&Lfa~AEq3kieDBhM{}yvNu$aBG%E24XvLew{QGr4 z1l8om$e;j0BNVhvUovm*TMaUy_8^NW@~Qd|bm_eP(WSmBrzKU8YJbDfjXr_DhmlzD zKFp66`uay}y8YcJs2_#Zh!c?RM43RPc_t zS(v7NHOj!#;rQO~!pk(4s&X)!j$9j+`2`#4n3qY|fwExqQXve>m&H@k zMwb~2-h|_w?kS@b6W0-#OxfS_Kt(Uiy4DRY)ct4N`T?Nm*GCdAp#f4q%TDV5@q2i# z3%VEkUlxEH?`I5v;9w~y&}gNrhw$4WI{kPfMQGU?l^*FV?B=&Aw9#q=3mdIV3td%( zbTv6}(OA1#FD<>Aq>O)Dx0(?a6o3n^IOp>G|A>0$=*Yh33v?zECllLFChpiy$C^xR zPHfwDCf3BZJ+W=u=-7Du`TpKp@Bd!C?!D)nI=go5s(tsjXkh3cyj0X~a*z9Cb!VcJ zYr>rU4W&qNRKDv>SMd|WLXP`=*Mr-ugWwq+&-5i6d7r6}l)<1M!lJ{L97T*_0d5|g z#T&OY(Ax-Rb=_d1p}v47yHdbD^TwMXd|>+W-W-x7nQRpjexJIndbjVNndePbj+AwP z|JG}9nOt1$sI9tJg}0#S3xTXQ;_WlUm0RbkV`94Wcj#9{=mAaz)4!u>Kp~pU|M4Tt z;zHORjSQc#+;I!oSro%Hjow7)a}*grg5{^hTb|#fyQJ;rp2fLB#d>;)9R%4?h~OW| zwr`t7Z@;4`Q?DS~;pd0P#S4;|zF>Qu51gGi_%TCaR|+(*s&f(D>j=ma6R}qq1pC1j z|9G>KvWaLce^d;MFt{lIq_UVqnd;IE8LKSIa_Fv2hr{^3GA(zz|G@Ho;d{Ywc4T+6 zTfE=D#XrCf2#&Avd2I*nAR~X!v9Kz$&(0DH%XRs-=?GM8%=lf}YQUMP67baWd~#&C{!QMR-k6p-c{_RjQLQDK_9 z13+7j)DMJK_|u-0dl<9z#cjCkjn;T0s5@;%RfKvUvyGbFf^CmB=hf!CvQ)dVoqV=< zRQ_My*F-RSrPzL!0C;54&<0%HtKB2akwvvLNM@!8RDlva(F#h0aO0Dma$ecw-kYwt z8F4b)-7fdKA0fER)~D*feqk7lU1FUdXiregeUmztH^Mtj=mP_qzeHd)AC0h(4@Q5E zFswg{KMj;GlU%A;P0C)IK9klnf85Ipz@6#z%LPoJ?|$>X5!3D;w+R}5IXO{MW(wHh zh_;FZU`eCri*0xR>sU^o4ATc@*(sd{?W4hvaT&X+hU+tz!{E~ZmdSX$BUrM zb{6vL={IK=8DoK{>ia!ej@q+V#1$=FUARc{L+b(HIh7iVn{h89?(gyime;$b>-9b5 z{voq$EmMg(Y9S%%x7n&Y>ZN1rF;CQoBKfL*&dWE%BH60x$%S9ddzsJm%m#1jlSZUB zx2){Dq)Red9`W935_;l?oy1q_@lt^qoi4ON7-57kGxdr^o%22uxz;<^aad zI}x|`^`UWqKxn?>pNIX`{=6xbJ}!f>ta zf`{s-3W3pm*$u?_n3{oZa6BTCi_2m*ptot+p#=}`-ww&>mfFDqOG#h=wwv^P*ZYn* zoVnVpDrj21sQvU<=xU(O2Hsc_y1CLcy6`K?A{mBCohWzP+-=(hukQuqGlpTrLE0HK z4dUjL6y;}Rnh{>O`h&Wx`Rn~^>w5_BhG>-gg`ItsaF-|ME|tF5Mkp6U$#U&i#DcE^ zRh{zQCBx?a$NYLy+=vDg-$3L6M4kjTfTlJmX?t&`9AV*soMve!)iTZX-hLY zqE7oeFW{sMPq{S9mFHYr%wX7j6TN&uHkUnYkr8hH3&kYUB{AB}aKv3MLS;bi_K6AC zPTnlv+x5=S^uj{$fQ;^ePYj1?7RHEo!izbJ#4f|5D?ICQmr zhl)|$-TXL4bB7d~><-HKfQBG2dY>fn@TXj;_Bu@6jTF^!?=+DVu*2i`x1E*I+|hDpaecl$ zU%7R-eFi4ckgue3*%SEOZ+#95cx5iZ&@hh9*2H;IrekifDO`3R;`eUxUL(waqCd1t zF8-0)-OX{>S(hdMztq`(b3wqBO4#{~RuPXO>qs`RR_35r$JG}LQY{#%W%CLT zC5Ju9@54 zn_}vt5=}wQE}JVP z*Cr&aLC_-9G5&O-&^5)K!PcWqjrCPS=Qrvu1skvuL!v&2i) zTn$?8noUZ-u{|s9iK=e85P^*0t0~%F`U>sN?@QXZ&KNu*hdSJb$?J0c=#~dmMXdZqVCwnGg#qFOoEqdE>P0b+u4NG$N4FJqvaV5 z&(#L$_RaeSH#EC#J8GrLqgbXwL<#Y--Je%d`3gmd@Yv4R{=s?x3Q*U=nCT4iu;>zP z2d5h>sYi#f3=iLE8`Ow}JTCq`x@-M0KIPMmdq(~D=P^jt6DIzb=jtK@Q(Jh%xJzX?! z_$7;4v!zK7PI@z_PsQ!bb~P|)&5(tst-LsY$3vaZ>FMv9fAtp@>dyE!mu~U@>H9-4 zA#TW->ii$@RLOaHq0zDHjx)9&@(NlMGn`~(=sw<#LCmJw(1$r%0}J!fEg*o6cey zS=?*Tqc>{1Ylj?qk;L^Xox)xZOt%ba6+ZiYmiA63Hl3hmgk{wSM$@I)jOz8k%-!{n zPh|6!7j(U;?0$rc*MU~@rT8(LZ#YtV{o94@XLzh?M?soYXUG1Jif1!`57@zg=A7NE z|KOj$O3P{4l-cdec-|P-p(aB(VpZhc0>o(2(BNKBYpVx`*xr(xgE-W^X0kJmM&{nqB7qIUR*?p?CySEKwhyW56?qL(c>V_;1C3_fEzS%=Pg>>Lv`=1jP5rTYBUjq66^jg-+S*M z*Y5BfuXkWa!#Ny*dOIYYt~&v*Yj^C_h8_87`bkjsHaW>EGhpM?1?p;?_0#*#{cq`X zdQ#+dzNZZ}w>vuLh8v~5XY?ae6_nd{bGMM=ttSor@EjXe9yjVuTC(;0@M@EF`&G)& zM+maua>$r~^H-28EODha9VfaxHLGx+yj~Q?+o9kmXL9!qO4Ehy(jcj3)V-zJUZ~&` zORZ;+vvqota6Huawy&T0VZDB$Wq7~-*Aw)c=tgM=#FK_;ze zpx-tV-LlK7<wQ;+WY!*+r$Q$_+c*btjHyBGL#~P#)?W_+x@9$MF z=TLTyw!%nIoU9Yx1Z=He)i%Tx5A*#y)~y!hg45`(W*0wygLuE}(M&ew%sJmPYD^?} zR=4azEU(&~{!vynVYN=kkzlOzSDD~iAd#Q+wdu>W7CpV?3 zHR?>X7`?`qi#OJ?veLX=Y!5H0CC3V3h1Gn_h>70TrtA)q#w0VLu1D#r%LNAbPGPn_ zv8viX2<8_#3|}8~PzEKWt*r@%y3;h{Xr(*Qlk|ekpuGN=%4DRSGx74GyKY{~N&9=N zcYl0CyJn=ROEkbgLBA=}N#Ez!U|+$?Px>z&0V zCHdQWy^pnAXsPX(pj|{q%MrLjQ*Juig*{u~ndhw9YiUJ^K;boR;u4LMz1MxTc{UQFn;bI~i8pR1qCf}cUpw?~SRzx=Uv z-7sO02nv$T5q)m<*5Z@d311&m^p`F>Pt(FrCXyM}YJXRY6~oWE3Wu5RZG3UrIp-ILVE6_n4Wf?tKBf+wz|4A9s}Og(_x z+s@;#8Pu4ovD;Zuzh3vIskWd@?0^<*dwqR6t<)<}HRO;Rg?Hn>{UVMdzTo%4Y6fct zG?@N75AI1|gTV8ji}hpvT1V1;j%9kvFN%=r4f=2O+`o=4i`-8&3z-m&&hOFN+MRytaQ5nlge4m~H;q zy*SSAJRj-xn2g!{gOZt4F*W$~I{)FkvEItM@Nl4}T5^H-5=p`Q%X;L4D#kpS=iy*=(95S&tRw&!x8S^ zd!uLF72^XTsFns?V)!QMC?l`DrrJ$-OLGc>p|_Kt7|D$Mde74x535=c{+;?GWPEqq z-?+{y$}t@&_u5{Ex(A;lvGba$;z5DT`M`!B?(*SqHsh)6=~Pa=imNp~7p8B!DO>r0 z4}2DN=_TfFX!%12(@~Sb<<;$&8JxEHb5CA;&#`(tp@J_&UA{-w(vfzzYF*%2_Z$CQ z%r=2Q>69J`p*KyZ8CbLb?NwxM2oW-8hp#R?{apmhW9Mt2%Sv(lw7_2>6OAz5S6as; zqcxegt;ey$@)V-iq}?aTGgrZ3xU78NIDHRbo_gi9RM<%3;}C>@5XYA>wQg&#JB(d) z&~+!|m9Ir!IT@BRn!G+_xrL%_pRi6+$G3`0*@_#!R{M4brPVW$^fW3+@5+BNccuhgB{&jbr9Nwp`G{)pQ(J(e}#LHkKf7pl)q z)%CR|O7vqojJ0Y$tmH=!7e(_xr}^Wp^R{SLh8l^YZW8=RYt%#x0kRG&KH?YgzOs<0{cJtg0!1 z4VOD29F5)*AJ2^z&Y7WLawKSeam1B{NXHvTXn^LsBm`mEL1*~ctRgIVZg!z|KjF0+ zjeuhQn^}-K8nD6@kgX?aE-gzXk5lInLmF!>raMwKR^S~dwd}tJdP4JYTXLOm1$jM& zwmfbYkK7?qlkX_+4Nr~z8*R=}TcrmaCDdgUsXTssJXUb#tTrWPL@7Eyp?=IYbM25L zhIKhd`vHZ>uMC8RV5#EA!+)SALAjl7h(0aWYt=T;Ao=o5MaMpG341(CQy`7>-ITd` zJr=T5lq2m9IfjyP1|#_u*vg*lh8|=KM(ttbDT;VbW^;e#)b7MMVoHm-M*l%cmD}Xg zhvoG$m$EmIl3cDuM@zutr-cDmH*E*q^ZOD%$l?)@_=oKbw}=xk|AlKicdlkRyBtk? zU_^aD%xp6NuI2r~mWFD_*4GnkNd7{Y>Dxnnb?&S8GX>|65fzP$1qlvUdZi9N#hh*Z zQSW|pls!II`tcAa!_0-uWH^rW?M$ivz6jaH;5?NK6G|$~cu$}Wq^yH-FX4lJSpZ3; zTRx<42^g>Mx*NbhA&xJ!fcu)k+3>}6^H~n4G8&wUzu|D^YQHn9u$LQ33@Rh%HP?`* z*ZU~qaaq(*w%_Eg$DDaZlFN;7UAlI2#ZkI#o}VQP;#_ti`n*)~f$(&#E;3)n86bf* z>Nc@#jIeXwi&mq<|M0wg_IDJ5VD{qP!~~=@8R}xtC>C);`UiF#4%>DJmNejxaIcZBu_Y)FO(s z2eybL^?8mQDrLvbPbGE7)?wsL3sQOdK=z5?eG-tSS=inEmdatxqT2Dn!fxidlRFi3 zEni}Kmm>@m0*4vI5*zvd3p20~A|Jzfw2`H&qmLyYK1*F|4XZS`Ll9YPM*`>ZK>iDmFD`#K7Wnwmg8!(;yp(dJDl_xP1; z!`zQ+ctnJxaVy6?!0q)X=i)Nhv{XW27V+Ay&7!VqgLgSB?wKq+ML$(!=h#Mi)r>GH3}3iiCcGiV3PWRe0#SL>RCU3P0<-B<4Q zZILkq2y4=#-V~6Ja^XDwyVi8UhKM>5yHy8H4Ss%l|D2?GyS~%UckQaDENNZ7SNkPE za-~hD3ccbxP`T7`-qoC@^ejz^u&_qIc=xH(*A`uLC`Jj#W~DB$$dI$_ap!)iZ42hs zz08u_*LeIOl!Im~_4qeccj&z(@~pudbdsZRW`1?+0~k?ZyTal(BJRi>c+>aPZ>aYH?)FwXm{-c|1-9*HLSiLj*|8f7G#6Joa zF&>(;E=g*szO;rf;pCzY!;Q)ptZuNmq*{*;0o1b}jQUKY^sPsWD?w?4?lFBoF!#D@+9gn+lFI&Ht z5^8({X2PlloR@skcMJqeSJmYt%u~h+(TZ?a5xDjvGGQjNgI`Z=STzH@un2W;ib;WaB@cpJcoS9W{IbeJegtdY`Kkr$pfBfP}@| z8=vZ;bV(ep3#mPNRR>Nrw)hrH>-eaZ@W}@3sIVp^qYZ5+EEeh&wyI@wtGX}CPc@|Y zZ@E8{Ol5yhq)JmCkHu%#qOG}L?HNU4>-X0{oKqOQ2fpTj^&zcdm_0fXhHS@>gzuYi zm#WCp<1O%^)CQc@DUWWm1@^SN@my^(ca?8^eO6;-mm@v39Ezt*RB9UYLS8!D;h)!2 z$bfU(!d{)r(uN8mPVWzh0g7Q9J|g6NCmjXi;0zVwUE-fQI+46xpN@Z$8g_RhgRcDz zn|vPEiUBqvSd6XbR3Yi!k^lrFAhyATX82{&a-Xb>PQhm;Geo))mjSHvH@vUl8+E%M z1*Nnq@K9956OMFi5+&Y%$k9OwQz{b$!e_A2_BM379~KC6wyHFU^T;5J)X2Q#U#*iG z_9vOJ#4~zU-Imv|J|d18l=#%wF88EnkN)AZfk@GIJba$d^lgn5wiF$|zRP197S^9q z?8=d;x)|f$_jJtN3T&3`U(sLLe?4WVc&f9*XbVoFH>VR9H}|c7t6uTjkY>`9D-A{R#1Sw=UT8*x zF{T0`;k4n49*fWVdswYvls^-})=b!rco5uJ2Io&!UYCwbI~T5t zyNOQ! z4zP$Hgs$1|2U-V z6h=%IcZ+eeogm(o8T)@%wTU#^3r=(t$z~6is48RTC&p(*RjxhEp}r_-KcASKy5n*~ zm#OeN#Ov`l@3>ukP#^CZ3_W(evuFBF^4-VZTH=H}9IjpF1_@=`W_sxDyU|cQ4rf}q zz_9G{l0DMT` z@^*8D*8KmQ*yfXwP+`}~BaEKDyO|sPU@e{JTj>vb+=%HG-h1T7#1B`%0AE;lfjdC?}FpolmE1 z3lfHlH!s9ZY~F|jZMu<5N6Mh&U{q5~b)KDqhC==H>2cE^2}{I9IuMR>IMIWH-OFkd z5**NY@fEk0t9nbC=ECbs6h%Y_m0vW)^Gd^U{pQS@bL3%`ss1D_(2&U#y!1slB&|$- zyqiRP!8Z4_k>|O1R#wzi3VeCNLQ^HAsWj^uz@Gf6WYJ{3)kmScXYVZMeRMs#An)392Jf=093krn)+_(Bwb8(subXK?+XY6S1z?3+}72c*Tt4j=Yd%& z`0;f|f7(fUE_7>4}gVpeJMTokA|v?jgePs3bdmb3Ho z^0M_RF@c=}Z&|e;+3$_1TpTH;?D>C~`nz*H@as)ujnNvt7JS52bSORqSExT2B@m~S zt>JX@-s2xAnS@-h@?VXAqnuGuHSqemJ5njl^2d_$H}5I1QL~H@x_;mYcD*7UO4Ia= zw}Nf#sc|y|N_xdpWDW3fm$kg&!k0L9^CFLCua~0}0x`k*wX&<+43}Rs;fODE^exLPh3zrFHEEM$U&Ga?WK0pIMsu#%D*)!$Dwq5nt&J>($>GjR=_ z_fd>l=}$i12bNo#;q#xXRw_9V8HTBfYzXK}|L!%bnX~P86^2pi&mGcQhyfE$=#4L< zc&JJ}VGyw8OlX#5?*-r$Q>%lN~wZ@xwkRco{UQiA^65kesBXvs2EJaE||TIXK<%3oX}$QUhD0GzqR?wNzJHE=HwkC!tn7 ztS|_Yde=}#C*dB5_D_U_l6nC5r^lsCLu#_B;LrI+Hilh*d3{|OD#OKB#1UD=drJ!~ zjGoMN!_+LcIHh>NsB0@Q3^1~+t+N&q#03*#Tu9T~=92Eh(e+|8G zPsWW!HhEH0Tdgi`Hl{AFoSaVoj%%Bb66df^ISvps)V&=Yi>;3~uF(1vSgiDp9ojuH z>qr%|Pj-0eI^Jt$;X9vZG!+&4a}D1&yLzdXI7N(X<~!AKoEg#Fgz3?3phsJfx{5zyni`_7BmVm!ZqgRRW0TF9osu;5&&@VSDn6DO z*_8LEL=7Mf{{e}SomY2okR3?<(*F4kdDX{cB7yq+k}9}R|k>1t>d!)~+J z!w)zodZ*b^7HMcO6h4XFWmPRA`{g&-zM@wqpr{r(X#>sCgb(dW2Mz|LH)MU_4L|@pxDCtR=_%n*`0DY2vT@CSXSQ9y;Vn%vu z8pY9?hC`Qy->%`)bKfG~5qyAahwdvbTdtD#_}=ml(Viz=dfcYE4$V63KFfD^X>wDEc`;Wxb#4PJjv9uCQJa@FABrGGhI>pJ#2k^#$dJ&}g78 zH~?3ZT!phE8yH$fxRB1MMVJwa9OO62;RaU6DV#jZEOxeWAYv%`Z`J9+)q@re<+C0q zS`sS^fiEL`2j^CzG11^&;1mI@Iv9ibKJo#VTVCC4>OC@k-{RB%+0-1s$hB0Wku?fBY((AI` z?uJu^x8h%x@;UehVLBUGe7MO`(+pFiQ zezkU=^fdOnh?F;t6mXJH0x1b1yD*>22fM<6it)a>BL}NjR+)?FIDWyVPY)Wea5_M) z^_=<&99@~Hse6vt#-8ZkvAroJy50fjZ(&7-Q(bt{8#<}1mV7^dCZ=KMWsxd9vW%+y zpi(fogo^QHju@vr7|&2tunt%dhU*A)3HScF6~C#zh4$B!TB!1iqbf#0aZWrJ5;R9! zbdT_VR6=0Bjrjkl1Om*j;&BMM@{~P$JD3kjks$DL+hAWIUpuU~J_4_=HW)VJ=gOpt*1|9LoUB z%c*TsSxX^0$SJ+QH5e_78~}f(7JkjCJk~c|jfk8v2(OB2kW+A*paAqj~F~6*2gFGt{-a$7a+{pJn)@j`= zxBdHVd+P;Albd+M7vwQ>G~V_eU0-dPZ+~t{f5?8=RLv9bbI`~}n5Ar1odev})RiD@ z8&D9P%n)VGIPnOH=kVZLs*+v@yn;F}bR@bmvhJ$rwhD*c9rXy01*nWLOoe=m(CU;M z+&V$I;D2naV_!SlXx13wBQB-AX!huAV9Fc$vk{DxPJ!nGcL>$l%=Q}*=2({C*P*UG zV=>sSCCc)o+P9t?H&jC+M%*`clk+UY+1OI|_h#fXf13^HWybl{a;u}L`yp=)v=Q>W zu?}3~U+r>_wknkToOyLPH`^+dcLJ>aF*@vOWsgm}_uQ&pNy+3KLhEF&PPBH)ICNb} zw+2pH0)>TU{%_Z1LO>3~Y=5Xd>bq)?XTQHuw77Qr*CB8EkqZQOI;wJ@lV%;OFUK$= z%<7IrP&40mDC||zN$f@V0w^e0v*hU|fqpwlFNRoFsxYGTZE1XwQITYPFGR9>ma@KQ zN=^u!)sX_Ow2z*#`HzoXgAN5e5`@H5M$7pS0Z=Jg? zcljnNYXhRl55}Q_J7;UX`>0i*H)&0wWafui0Hs5VL^n`w&lc2`Hzf{wRGZ@4)D!HC z&A88BOZFacl%Rd=3f**8@%{h+PBw~Gisut~bS=;=yrYOre!G}a*E4BR=4L*@r83y5 zVdhgJ;6})g^jbNS#UhQ{sAaPFqYVz8mp?9At zowR4M&Si1Hh@megN(Ok)ZkTaQ6AucqFP_}-K2u^$_0_jjuQJJ)8A z&|nuci5O>%tI;Cq_2yCnPg`z;>(IsgOxXZd(xk{{aPc}!$H4?(IJ{Q)gD>b$RH^@R)yY&I#sWWLLIlHndp#b89TF$a_K}nkq_8!IKD86v;#*m zV&rI7i<^NhZoha+e(4)$=;A>|#EJ#lJ#ge)@-&*b0NcCl@Pf+;kAqAVq~G!p>na=N|GAmcG3_EGE+jjA{ro8^&nf zHoug3L70=KQ?W@doBogyWu=?LB`hXYJT!t1MF0vmSy;pD`SCf_k`P)G3r{(qBaw}5 zqvF5scn&+9x$B@_Q~E@b-t-!mZ~`pZ_Tpio#y1r=l)%tY-xB#Ml!`Q7Rkt*AXwfrU zm|_Snx;q7NycOk@pLs_M*@6-BG(PIP&i@u35dwlX0R`JSe&zip*t&&+>1$3_Qpij5 z$B1})mT9j|I=br+A#o5RvTZ4p|4ARKYj}if*dcNcRqLQrW-~Z=@<8#z-IEJf?@RukDJc! zJqN}%F+Fte^>yOo=a18S*L5pjk~Jgqz6wj5iHH2|v93SIN0tQK1fu9m$1gMBNz;sN z7hR`P-nT8S(E^K}dR@o`sn#Ql^oQ?hLwVhgjkdZ=YTn$x4GnbZhQ`~#2qc~IC3BxL ztS92JL=Lv0H$fu|>4iWYf}w(7Lw8^EBaHXQmm1Cs)8Q4`DEx$216l2*#X|r=+5}@; zRZ`cBS#f#xx!4lmF~9j|u`9lVK-;kUZTj)vVV6d?Yv=Kv=?1xV?0l&9QhO)(@m}BW z4Hf~-rWrGjOhmHA9qH1l>V>KqkoUZy$pe#!ep0oPwzM z$ZXpkQldy&gA+0gSW*uZhu^hDCY<~DN}K=7eauCLf@2za?f%vgSRy5pQj!vN1&IKa z_hMB_zsNx_S%jg*Xt1<3rzG};8O+xM!W@TYokpr=Tj2{ zh*=+zbABI$xwCm}wdgog%mUSTb?z0AD8TF=cgv|NJ6M|%O9H_X9@@EpaVLnw&h*}w7MNVwtF)!e~ zL;-neFhqX8uNj!eyvCxLVoM!pFf9NN-StxaU(pg(OsLe?eJcT=5#gn_RwcL|PTxBS z{1{Cj{IMP{GY~ao(EK-RCLmiwEo7^bH^_5}Fv6s-x5V>Ik9N^LDsUu@YbIxwEO0LX z9jSqwlp)bQ?Of7jK;I+_#<~k-cJI7S+g@^TBWt$yWb(hy^p%u^H6~hCB(nIst<}~5 ztONj-0@38BIJB64kqJB8y^Yj+0q>3#e-JPh#t|4L<^x6x4AAX|?VBmbpuj-C)~Y)z zxkC*AcS8PKA7Nv8m&=87rSRYm4G5}LkzW~(g_O!9o;zdha%F8faWRU=LF!S%{MIeP z6e9+P-8=fffD9x<;TJAHsU4zNR7k0PQv;sk zarZIB9mal95XW9Azw^^8e+urf!zZU;yixaZP z+-BM*`!4wIWz4I2WS2M>|Hgl#y5~JG7-L_T7OeY{rbSe*PIg4b6b$%7Q?y$HgmD39 z2H*OmmS(}HcTyj6d_<$*1>>@ute<}aN^&?Ou|8e5RWgB_B%a9`dC~3qrAeCk5r;rK z%p}`+wKB|gb=yT<1u2M&ib%!@cPls^MZ!dnISN>YjI`jXII}Ct`#40Yb-lM@k7H9U zp+9v?Q>@^(up_`PE3&WQ4Eb$X#}10a3B&M`eC;|=aev`jy1Y&8=%A96Fo}eDmT`7kvE1nMv9`j~u>-HB z8#hXbtO|Y(_@)X~yjbz<)*h}qW2^#&y^isVkhHW0>XaIGBeS6=HCFf`*wm{3VHgeA zFu9_@b${4t=@TLCH6Wb8Z*kJgx!o+usKy_ILcodVR0JEC^mkILWwltnF0Fu^@zGv) z4FR_dI^5Xx=m#WxJX~PSf|2mwxezSrJ?U1WW@R_(+fLaQ`4Hr26JK&8e}vn>;o4?$ zaQS|#E32fER-78f$dS56BJDm_-)5eDN2Z-6T*lj{En@sKe&k*9!f7G)d>WhV+*0&r zd8I^wDE#d6>g$IqP@qbS4WmKCFDEnJ+-BkR=l^g6mm_#E+0><-{%0;)g`j%GpJjC( z0P?{XZUwQTknH@VJ;1TPeG(Z9dcHjZ{0bF#FQE?@D;m;43eWYWTcz$ob0pmRP*Tm$*`?&AM$P;gl6X7 zZ|D47cnV#en8!HMphL-l=ApyTIAr{@MOr$|kfe+SFD^ZZg>p$9xe* zx08=}&sxRI3uMwl0hc}@9MUH(=a-lR9@C0?Tp+41LD1~t0L4MYVTgem)UxdYsZRStlPi|Jze{QGve(8 z+0pUmCq<0-fHZc9@We~};p=i3dS)swWsCqqtZl_ki}lk}lt@D(LVZD6kO0YC=>N{V z$Z|NAV^)yZffs6GcC5S_?Gb639+{)e-Ec6`0fDJGL+F(9)G<6Y3HcX{{go$sa^5-r0x`Nia~be-ugHdz zYp1`tKY}94NTf$0N-7a*5fHQ)t2E}>O5&Z~<6Q$4IwApo8YNb-FGPqza-TY)tnd26SN_Xubq@{ehy@QQTyEEUuO}(Y|c!HN%aQN9vR3;^YF#|ns*&krqh=QL$@$A z#F4^c+T9knlHi3Hprk3MQF3|1GUdD|X!@EZ?9}vH3afwlwe}jDOs7bnaz>4nL0Cz` z?H3NJ0pt33tPrp_HT?TDPWL)}oq8B9iinSNoE5D7d6u#2`j0fp>{bk)xSo>L7&~uV z&64;y&qT7Bh;{Bn%zBiP)iF{#K4WzX&H&jb{ugDnD`aec`G*79QNJmo*k<=wV&bG2 zNa?bUKl0{s0dRjdEg&bYAcMFX*3L_;O8R8lA4y3kOVKK%Y#`;~Pn&&~31Pnvw~j7o ziqw+}GN$t3t(c#%u@{i+;lTCxtuDfNkW6Pt#T^a67|D*Ut8FpN69n;W7n)N4AcamG zY6m!^B9#8vI15}UY4yU z9{MJb8xTiXp%tMUjxD;Pl+g*7WXU43GHRX62WlwFM&UJGhi)+#>$|aFL*`%Ejuqqe80mdM$^tj=rdWNIEx*lPW>=hCoymYyq1^@V2`eqkeJrAd3DSuEP|?|6VI1|+l2 ze<%eR3W7yxl&u&oeFpXUX2{G_kn&segaYvXEVfTe3tqx7J)y%f>CXwCZkZ=#Mf^E_ z)R&3S^;$NWF35{|yk&YezUYM?l+4}04C%<2ZvHQ{Q&llITJe+W;Pg8Eofzw35qbF z1f=xECNmI&o7ok}myn3)kP*7@i*N!bm4VQ@yehoRm<7+g)P-I*HAt!NUA?jD`+YQN zzf${O90>PIW4!&&V}lS3^WP-4!KN5_LQC!cAFD*-v7GvwM%LR`ro?{RQ%OYMBgXa)Pz(G3#otpv+aes{ZAouRqI%6~afGfUnJ zjuvZMzH+V1W8ofckb~Etn#D=SfHJS!BVvJLl$ zRbBj}!(X$TykeVYd#%Y`48?tB1m@X+&CNIsy?)X%5<0^DtfM`~w-UuFbT-^1rxGT- zkh@YN7Awhx=t=h2AEF`=q{L>(pHsw&>LaZt^z~^b=+@-G%Sl6veK|n>`KkmQ42YL3 zc|QKTmx}@LOgJyMIL=%=@G=ewWfzF<7Oq4Mh9 z8J(;k+_Jl?xHCKutGg0zJ3%XCN|r_j;prQ=E8UuIJ9e^T+qP}n>5gsNwr!_lJLz<6+qT(p@+IdT_x^xA#pBM&R$oWP_Jn*PTq5klj^H&S7Zwd>_&VI(AjX+o`m|-lDj~cu>>) z7Y2jTX2O`n+jB<8TDwis;Srhs#(4mi`Uk`XTvIK%5r)=}&D;gaIYKnj3MK0Jw|o)_ zE1e0VnDMAllK9ksb7qAG{EM}%0Lx<|4knn=nc)I$3y6f<5XRj^Vp>|7X-nbVxT zN9GYFWMvBc1h^R=;g;+0FaAl#9RZ%N(@Whm<;H+4Y;j}cemMv-u?mcP$WPP)c5cn& zL=mln>vVwU$c7lP62NRLt@ctH_+?|cj0xghDYVR-SSG%riQK^4ocFg`3$d-sX<28u;S!M8M~)!JP$sU zcZl2vh%!AE~JMzR- z1FFM}KPfy3C{kt~gsL2mR0wHM^$ls5$1P%M?w2|3M9QDADg00c3!uw4c}6lg$zFAO z9Eg`DdmQmpEVxo%ZOLyS^#l^q9&>j+U#KAA=_-nPA16_WXo}%CkJS$*dIf=*6*d33 z-zf&h>?N^S1LiU^bey`wK_z2Qgwqow7!$);Reb&GVOI*n()tih!CcRO^mECw=uQXcm?SYoJMF96loI2RsYrYgnDu&@M7#QOQ%RY z*FKnP-sW7lpu>`KrO@vkwQmw;VpSlG5|nas z)K48D89>fQrjR&WBFXJ#RiuIKC3gqa)s5#O557dBkl0@&nn@UhlYBt`$k)dGGA#sPHCQd|FK#n0&%3NlcFblz$B|LTSqF00Vrp0RS+<>BsM* z3n?|S72fS(1}6S=rUpcZm{>&Gli zP>0!6n~b-{|1_aWZ6Wvc%mFqQXsz}3>+7RW!4+XBn!K;GeCA?3A`-UWm?bkfPnRwnt*F=urPx25<%wrHI zmd07r9tc}$;tXDEiF9wwax(4U_;mXydYc|?{PTgSDa;P7_Jp%CGWdy}{mJ z3~*ap+`*WPSWq(cJ4POJb@;$b!u!9jX@mKnnFBVkd`2&2Gb&>K2B8oAMW`|6I+pc*Kfc41w%LI(V6_x>G+M;($$JwVUy-}(hQx{ zbyGANP-()OJ~K|#K=6(vl7VO9(D-yu1HF76)(|wUQ;)oWy(MC3%!y@b2`Fl;3giQu zY;{X&0zs&$=+WJz=y5->*I*%p7?&1P^@YWqjjAp0<@oj2jKYS;7WVMXnEgXLF(L{x z5|c9kwQxu;=D^9CGX>*?_6h6cb6O!nUS@1C*L9(wnpC2Q&+0 zb@otTIPyokmev<1V@&3Zn5obxu}TT}imgXQiD#7~BUh%e(x&7)P`)c!#Z>0M-M8}Z z$z1tcMhpG_vmzvLOvw(iwzdSgb~Yj-cv)dgPF@$@Kl_UIhRmXshPEMu>v7RVV#W~@ zqK*)kb*grucg*0-`|9$eQ<4ZvXu2UqE0sb4t=1+~l!@`-t zI1oLLlHr4R%e*i;W`r>m<6wBCgKu%xYsW;CKw`kmLcE~`hR1QfFoP95Or6L6T`!!7 z3y{%rFa3#rJu6+f=pH~;p1$6M23^I1vlg-?^H*a2X}~pWq7#%Rnk)Q1JWvX^I{oV> zjBe;YvEDu4kOL4M2%Sqs#+B3wp@luu;{_qV&HS_=W zJoAK|aSsI#7A*fhKwO)A5VEHfg1)Ia>8!!tNk7j#KxqNYhYUryf*>j?m`tfI zs#LT(RQCBD8)Y?+9<3by0QJ-ajJeNHaL2NU;$&9L`~gTz<(B4mjn*J+TQ+;r#isM-Hx zWq#hy{Y>jg?!?d8^!tS%0swz$xEubYs;C;)9b<@naefHE9=AM*X z@~1F_JDol(fRqX16n~4+(40Z1d9xYl{sCq=)8X7 zk*Ec|$%KoBJc<^#0!J5;hcP#jn(b%r*&4mNNE0ODW2Ob7@6wG;=q(=Be&K9Yt_R%C zIEBsWr@@L1Um3>)G0c+F2f3~*skjajFDVBcPSg}O)d`9!*SD?LwjrxLscMF1+lODP zpbT9lQ5uo6>^&`<~Rr;B1_Lpl~=fcP?#3c0kx zklipV+9Zli=#Uhf_L|=mI=*5*P4+rP$%pmSd~~2zb=4-YS?KH-@sVsai)}9>DlkJF z#afT|!$Q+n)}4ffPcV4pzhtQ!b>-&~Oaohba{4~PhI5&)tjk&GNy6pqrynodUJJX4 z${VC_8SUrkPzKwe>wxqtF37uDzGTGIW3t$?Bh(1Eiks**hT?>!islUcX%bFpfW{y>6EYp6#- zsHttIG|>AWT>PFM{RRFniKkU;!ryb52Yd)0vN@E^i)4YYaJ?;)SPXL59WN3F@m_Clyg5LXVRs6(uGv zM1S!y=MDEX2!?ETS*sBRgCD@U9McG<&WHfQimSUfN5qwwSKCd02AYraiAe)G#D(@Yy5CBSQU&^_P zcCVQYGeLn5RKouc&t(OsA}QYpxG<)DI_Ld{ZJ-+)2qYp)6@>NWz-ckSs*p<)L%Gh zIxwj%Xmt2={igv3^2!}dHM)uFZvg3F4E&*H9PE&g9k!^9Glmz5DeNrS)X;4SN>r)! zKokx=*4nw-0yi`@LgO5|9;699M!;(FX9?&uOLT(yzh!CiLQ3bC_fCcCBXJB+O_WkW zdwZPbEJeGcC_irA&<}L$>lX~*W;fza2sT2Wdzzq1x>k29$iyE5&Ro;2&g`?dA$thu z(cOVpc>Rrtm|@1_>e*oz%em2ALd14V0KgmqHfC@_xzu@0wXl{DkC(l$Uc7rL`NiS} zFJ+e;6Jqs0XcHX-{hcJDA`+5#KR6t8pp}iP_@zWnNwU;j?d2ErVK>tp0I308q; zh$`1rf?<4~{3C3fw?-|z4E0_5dG4@X~YlI9W z-u7a6Ti-{VBF6VO{>{zUny#4u%5+H#hf<`u25XE93$a(h)t#0W0+lvw&BUXfEJ;U2 zw<)aiYN^mvP3hb!ll?+1gf4d%wCYTt#u|6LDq|V5eWL`Ng|@I@5-BA0ACxS2CwZV- zc$s16qNzJQv_8uq){w!!p7wt7Sy|ugHaWb2#kIM=3J|OLoLTjwT8WS4z}-jc$yn2Y z52>|YCyvw(lJ&%=SaL`**DJv?E36$PCc$Gj2(~bGdb3ESl(G z3u!;oHT^hX2=Rc0iq#_sDmnC6_Bz_GFO$CcOD&BF+^iNX{lKu69>=tR-(2N#3u3r4HoU4KvkQE?bbiSTE2 zVJ(;cMhZ{@q|GOXIVEaKU#s9EMe1ei-Q5(87_hQ4QDhIV^xeaA+yDr_DKtJ^)M zIy8gw=;%AUUf82L?s1%ePKwWM>wQy{-Dk_8z1`QA?ZFdF4IdvWs}v&{TA6^ycl2p3 zd%(XS7SXHw*2e zO&;EE zU+>p8UavdZ8jM`HBRBU8%qcX5GeB>wYjRTcOqyAcrLAgC?$oZ15F4Ifiu0oMYs1Vy!-y)<2u>sNgZXN72NatA}Ja(+!=scE%vbA$WZifQgRF ziv4CO?5~$8TDMc>J-e0S76J)moWBF%?X@tTt2MTT?MeaM_AOkmBO(LanusMp%0U{ z{S_sZh30rgiTvA)tLZxUfI#OCgKZ->6@9PDUW#F0qy@SAGuQigxcf_I-ml{P3)qi> z9A399{9{s4gDh`T-LGy6E12*X-uQq8#(}iJX#={y*k-_5`LK`Q2v}#?E-um#*(zzD zc^T29m~)1a79!Wn47)l#D}Q7BW&ZQ%N0;xW3W=<# z$$>C8z)If-Obl-4-0dlhQ$Hd|=XBW&`nqDEFfLP1a6;NQB$rU#m=9Hmhs~5EV{S0N zvL3aN8>E(Vqn2YGk|{mY!Q#@jn7>o{E{w_B0K&k9R5T5^f&oma$Plvz%P)=eUU6S5 zLt-E{4eIqt)UL>6md27A&4_IKQWUK9bpZG22>pm4JUl>z*>Hb@F%8x@XH`7$CGPte z8~}0aP(T(C?3Ziu@cdEm?CGGMz`lFpquF}GrkNr?4arPGVfLV51_FYaL5`~S(j4U` zv)}QpRS$h8>ug5&yNQ(TQLK5lGtq#eAuVT-vQL?(VX)T%IEKyq2nu)YF!nuJ1o@|3 zigL`5k6*;W6PYMD?Simpq5^&tMSg#P7C?bYH3N5H-m|u0U`k`#X}Dr-K(tm{vVS0= z{*M(q*SWouP>vsUNl~8lgEC3c`!9uTlF2!{o65RYlT*<3j(YG43hVT#QW4#(%-~Nv zGqFr^zgIqHxT#9+F!x5FKbAM3UbpL%r9KaWLA(=2kQ`v6R`}W!Wi9pA^46RQ7LVJ^ z!S2@=S_r<#$k;O{nh9N!TiKgv{chOtnWD;+Q3B%uncHE~0tVKMr(5r_<GZb1B;)U`Gbt!4Uku`s98~WNLZZChwNj&{gw6yQsbly=zOT33=aIlsI1L_D z)BSl6i4J%1W1#VZjP&Oj%yJ*g{93&CS$%ChaS~T&JFqyYnhyQ>nqJB}IzL0AQ7i^( z_4OZKw3v&!_W7T-1+a5;G$U(ta$*a9F(uK=PS&BM9~Cbz=XgaO$X_b*dAAf}f(go? z{|~eR7;S>uc{ef=$%}K7k|}Z6IxB(MN=t~>HiI_Vz6q25x>Lnbh%5-AyEtEld98kA z;PWNE##HV3mf~bITT2J`oA63?2vQq*b z+<~BmU-9EjFah&xhs3r4o5^*H$Ko|nKbQ76(z)T!61yE@go|wuJ-HA>t``v*He)T; z@Q2(_L+nd8H~b|&w)NP6j-d&XwVMZOXuBb2*zVmoet z#iehQA~3*D#vY}dfH8)#kJFO1WiE(32kJ*bWSE_XSMi!=uo58S2}6{OOL4~~?_(QJ zL)yimx2Y`1>V{J^jrZ`^cAK}G%?6MRW(xfLtJ&O1-i9w+wtsqT_axs07{RPTY_!C& zSq_THXG)m7Kx%*hnVAJ@kiXe_D-InOHg^IuhNL;V(7a0jwAEllzpmPHXu$ny*U3#pLcG%Zg?qFiceP-aElayGLkQcmoaRrPs)ljdyDEsu z?Kwql$S2Cxk#(AiX^D2dU(+z!GP(Wt^AskghmSe7d0)(B`M&gXzno@|D~-0)O6%s!|AKFx?lQ0{ z^^%I#(t~JIVguCR5Evy)$keG>snMVYA_%-- zKP`IDp#ZTnAO5ax5#Ti)Gmu_R0Dxp|WI#j$bO;SyjadDdXalhrHxeXK(2l;@u%wQu z>1B`@7sdouN^Ir46Y7NoVtuG~9s(>EuopPd+aq&lBg#W(OdYajLlF*zHF$+`OD z8f%VbYiu1(4VmBzdKbBXxSJ%-l5zX|mgkVYx!4W{e$0O?Kv+CrCjD?R!W5Za1*9iq zl4EOPgOmOWHhDQz)pB+x5Lna;SLNrR>fVmzLyOC4aXTVPmT}I8Wdfww}&k=y=Nke!% zjg=Y_1saziblq;@h`Oml!li`S0D({fT*~y>H?N^f<#i7hF$reR#dEu{R`3iqMh;%R} zzTRgEP9g3LMFxUd-*5bP?n?lo(uHf@WTvjUWF)kfH$C1*6P#lgfXYzK4KQ&*+a3AOga5Zq5AJqdn`-e+XH*7av z=VZ{FAbdsd8t|58JbAk~jh~Mi7CSWj20Uqg%e~0I?30JL-ZyAemKE`Ht@j#&YugE< z>(}Z04^QTkTh7FK24e#vo7W>gD-%FWCrYTk$NZW7t+RjjFA-7Y9|+b)Mbvn$uF4>z zKAgER=XDm^XGH%XIKaym$sV__{MM=r@I~SH$=o+YN8rq2trQXQuN>nYSc71WIbNJl8`d$n)N+eu~tC+*y z8DzgIx#LdDwco|R0ZBa2CriU=aBL}f-2Sd|t*@2Mz3A*gf7L%+UQ6cY?e#BlN6#t3 zf(=87nK_e#ibc3juL|}sv%3aG1gcOEQ-{@3P{aA=FHaPLniIp9Vw#5Ukajf05|CrJ zT+n=;hFy5tgZaq2!jL_E9T^~4XeRu}HK-R;UZ&R%)uV)b7hbVb*8@A5qGk0D3JG3I z!pm-`MzR}3*y=03!lvN=!X_aBa;i*=u+q9)MQh4rUsSI!_vhrQ5+1K0H%6jeN{1o; z5Lel#F@AZ0iAVZQ?4w-lV><$>lt=BCDTVljQIpUdrg%FUToxTp$yx^_0^z7&>{Fap zS_ZwzYNc@#P6Jc3LXTreHWcc^T3Qt{JC&|E6;*U?VtRqLAF1f0@NCM>FNVgYc$}xl z8(W#oHZck(UJqY5UQ35Sr1~Lyqv})P+>=)rF~r#Nx8l}Z*bc;|!VQ>_=_VSms-k#n zuMaT>V*WsG23z0=B97}gJDXspUy2F{5Bj1Ne$`nJK$8kg;$%adiU&{yq$1>N8j@V| zkNxf*;LrM}s(b(RJ+wS3@o_=>&xZ%6N29A~HQGJ_n4s9eU#3|Cem!g=j*>IYo*#3V zMrUG+my#%M5@;e$w(vOPf6cP~)wdDzOFWQ>pYQ(#`CcFKV=&4nVS-q3SsbHkmL^Ug zrJqOq1mlJMEOJCq)j3RZNkjzQTwp`O)e!tu zFG`FV`kfcm``5Sq7()(&rlf2Oq0Wu4dk76j$?x}9njG$-$K=+}_(F0=aPUJ*6G{ha z;q`YY{db*3gajtyYA^B^)bygthRCxCyXG!QvgQu`=S4L+!G48_4bdjz*P(T0GJHzN ziH$3Yx*Bl1jD-6OSVgNSCOuD^Bkr?;TS4`sc@7pa&iEa<0h}^7v~k9An~{ZRYLa11 z6|wWI;U9%M=&C(gR}~fkW)|?wy>zTZJiaU;cgt{<1zZc?ys)n>q6i}3R)y?Okpx{Z z*+?;9i8{46+@+ulIlHj;N;c_UD z!L@a6{Js{j#^;kppiGCq5XE|u&i@@J^NWSMpy;8w>sGDia#Tu~ld9N-;5k2}CdT+B zv}1=0{?y`D(7IID9({4{6wN?^8&Kp&Ea~;SjL*kWZmD0CqU7tw*zwwe1@Ga&nxIL0 zP%-|P!2&~u`t!|sl*0x_ij#1lG*UnBe4HyF zCh$ytUoyF`7W~7w2twME^c3@wgP9ggl(P-elKfElmS~um(fx;Vp20o04(iAsrUTUQxp&^swmYnG8fu%;gI25g|0GDMX1{uO2WA3X0<`v1;8 z4;yGwhiE#c_|N6F2s#x>Dll@Xqzy3X$z^rfV+=Ns=8sk-z}!A&DglodS`LNuz>#{; zb&55Y68$q*!f&0hm>&(C;^PT19n{|W-<1vqa4$taL;p#RFsXngwL$tHUYY2CSu&C1 zh8%RvByDox5-#54s0ifV^#Wj%RQbG>_B@t6gUvmFGye3qi2T*D2vVxXgYU;C|98i zCLvA@_?vy#Afv1#oVq6@5T0Lo&Avo%bUvoQ>_k`ne)g)NIqG(9Y)v84YarHR9~)OwKPnQ|^%_xuHJg z4MBzmr%Xm=FlRFy;Hh2sH^&GRHbl@_$Uh=kEBKMEg_wWJ+zfR;{h_2@{x%GJw)2a2 zX$GtRN{7F0-VDto>gQ_>%WgA)RG|x?>B%3cfvbdHVW4ZpQpI z4wPoQ4^P)e28)RIcYDJB_Mg9GeEhd~|3Xkxuigxei4n7fjEVljSxtXn9y}>Y4_vN3 z{mv)8#D{TNo*h@RIaD-55Jt=6aTvLkj8+xHq_LZz;Knz3XsEHD--3j=MppIsG7FoE z7s*v6S)l6;aS(r-kWaXdp3HZ8|As&wVtmG2qP$_{n2X>nc))KYl;~n%Q^NqmJJ2Xh z2W7qF8?3<$X1)kNz4@X72S5&MjZoNArS_K0g2!1I(*=V%Ei$GC1h=6S1<(zJ{2=#Y zBy}i0AaXt>IzJg6yvpVC8zVQ}n}mtnrr8LKc|mGy8*&_>AlM8)zD}Cf++S{=`NAu) z{E3Ob5)&xY+Tzx)%?h2hWO3WBzs0?fWtghfu?>JR{xPp2PQ(Cbr!iVl5O4Mq0$@eL zI&>Z$mJ$Q}0JEdM6&23zIw5@GVb(2?P}7^X;yN)Plp}1$VxHw*Xzd|RLAdrlDaTUW zk8X9YzJ3mbhx>ZfL~ZnUhB{v@#OEr{3U8G5y6OLNZgU|4GK!l*>wlneBB3UT78oB; z^^O_f5BisCEKn?tVH3E){i5o7SFefJ;lKaoFD^ZQuDrtk=qP9U+&VMfyiYhu95+gk zyb2Bq0tJ?cCo2djt)!{1uCNpy81%J1Yp*V-C@8Hh0FHnRb1EbBzuzQJm$`a>`1-Z$ zrDwWinhdnjVQ^Mrr~BiE!)4oF&nwrikDaMAEQPI0!w7yki%mSE%s&hdEX0S`1i@$! ziQ^8d0gh~2-n{=vgap!L%Ce`=E`}^$5$aq5FcF~)nr8#p8wvKO8OL6S+(Od+g4F4u zYbWERX|ZkQMP?Adi(06p81pf$0uHs*Zi%}-3W|O0o#_L|gDK_lZqorstK7YA7jTQ) zc?x|H(om46`by}e=%&B&^tBnPosM;_6)_@0bQZr{rWh^|E4k9%4tbrnV=0WficvQ& zKtS5G;dYD_GUJ$q!#&3u_Iw+bIjjQ7>q`*9`RYD_fuNtoa+B)K+!wNiza7H&^V~s( z%5G-_PPX-8MC;ej_~_aDv077C$TRF$``6on*0e*%IT#k{hMZuW8utX(m%-zYNmkIP zRVcLiCbq)5LYNgwxu=aNd5n_-rYANuO-mtlg0M0$QJPU!c>NMJ*6?D%7DZ8ka|mHe zT#`!IK@a9CXf>8uu#@5?&pHeD_KbR?a;%uQ*u`#Sp@1p;z6SbmfUr8{AQ?rz-&Cr; zY}hB;BA%<+OE@Ph`M?gak?fg{9r_skFY^?F`oxFa;_2zlu0TYOn?q7gM~;d7U83UD`0?}N6VSf6fvQgRQV{ulEFUeAB8$uR@wmPM|S$f z_xp#duNnO9ZNs+n?>@`s-IG;3l><(Fn<}y4qoZq%t_E2O?C4O{Tw!9n2uMg~%EejU zP=rJ@1?*h{;uF1W3^lbFYKPd9c0H z45JsxpYG^M-Gj-eAGbBF_vfqF{ip1n3nP0ZPucjxlJ^vIF|F9aLX12QLhQ-~Ok!e{ zP`eKOzD$oemb@JQ9;fHbB6}5B?(4sgf(#P!YHMXkKntm59R=pMB+RVZJ9DQDR2rs5 zXwzTJo*1$FCYDmkrCqzk<9IAGdu%;6WMt;#+5#lW2APju&mk2(#$KaAT*|?J`AhjG z=uN*@x1NZR6LIK4`MNN!Y@aUHQ)Rl+%M{kO-muG4fFBhY(%$CaBZ@5qb`o9ANjd7JRJT8lvCTE5CFy>TQK1K2f==#Pr;N(>2@N)jaak}nq>+wwGN zE#8QmNfX1BR4B0XYKYDK$So>dNQh-wwuP9REIA3s{UMZdcNy>girLT4pjHw9mp~EK zoYkLqZZfGs1naS`X0?i?@REOUR9xnbl=05YP}Jf?*^cXeC?awF3`>~b#u|YVLoG%q z2&NSo*L{PP7=FVNO+qp(*_d~YR{8+_9p~l|3H%eepoKKO{Q={md(%hG+bMtXd!s6? zWX5Q_D`(x)*d{^+9>ngK&It->djkZU#l1U;v*VPe1co6@S7i2S{-msEi)s)EqN(E% zPWSjcAzXKd=mSr9xA*Q4kwDJK-X4RW=qG-Z2dnR%`&E{=C$FYyGA zeo6OqCnvX_9RzAHFd~$VFV0N4ftj;GgX*?AVBvZE!P51~$E{GBP4kaHHb4@p0I&Qa zQcog*CN(9=_P8hf@W%Tz#qA1At_PaYK8vcnV#5hapa?Ofi7; zOCdlqA)0Q5sDj_gBH0Tc8M%rToC-#+Rp`oS7D(fhD{V!XC1|n4I{WS3?H6~`>SeKUl84m-m z47%aX=hl#gAu-j@zF9g18jGsiv9DD$XIx_Xh;>LP&o%~pr>CG0rl4@0Bqu08?vL;N ziRX4Qx%UVWmz*ARFovdok`PQ$o)&y)dL`akoUuf214**2rehgIrxs5@3(O|R!}JWu z*9clOXG$1zVmMZTYZ1luR3~8a(N5bqe zak@u%l(iJxezr*9SAIR0PH#|He4W_2khU`Td8aU#gwS-MS^6$MS3%-4-R~>MX=u?9q z@1gc15z>l`?>sz@_n-N1z1WJ6%dzpA&UmwA6JWn+x>2C|bQ`d}n>yk+4LPmM)5HEb z_a!7)SebeZbbEx+s^FKryXA6QbjIJj+1rm<~ z4)`!q!1jD#H_^g@FcfSgC~c5EUJKmbPVmOF4*S$ErAtac!u%nt-n8$IKOZ?07*BzV5FGPaQr#`6`+a%XPQ=%PonJ z6s>z8hYrbwD5!pmy}T8^&y}BbUMIj!Zx1y-;0KP?L+DpeXdiE{xq15;Qv`=HebkmM zdKa0Q3W_=r(rUEgItO0&ZXj;n6KLbP+#uLXHUlx*uMD1xER`I-oJzmxLijCu=4z^r z+RK*PUC5g+)&q+pj|@)&jF;#~9*ohk943(}%SpbViJk8}U#M^7S2z!NcyuV&U7yr_Gy zKOPIOCq7vc+V+mFFGwuk(_apO5?mA{KG&g^I)f3_V=K$A<&__RF}ild9t8~R3EI}( z0@XC{4~Uz>4&OlbMVoF?bejHHUk7@ihQh)oxK!PH#c_=n!MCT&t=*O1x`WR!=sSC# z4&*k^{pz*meQN7Y@5ZesDN_v!$Q!B4UAG&$+g%X3MmnziUt4M;TThy~7hX>L9$HT$ z)&P1@VZo~u!iFVmUsv)COA`@f80%v_N;MO=Tr^7C&7 zt1sRcR1->I*^XNxb&+{zkBk2}eiW;#S&jXjK66a(dp7uQgJygP6xF7MqJ$!7*j#-h?0VQRgRBw#sP8uu6U&HD?Q2ZJ`Q?wS(>5Rt}FfmzPiOX6)yo_KLO0@7x*E zY@_XVVi-lNcYRQBbr6}EM>1$|3h5IL79m$gclV=E>1k!h4-FK<3CAHC6~r`KauIu6d_|b0(?8XsPH@+ z&{Y4f$1Jpr4+fyJ=N%mI4-gL^iFl0DAEf~eTM_x&kgUnLVn97sTd!Re#m&mqcZ4O&afFNl%D~fVLY^ zUD7jf57%PX7b<&!2gf9tXtp1N=Xrgj&a!G(P@gJrp(sZ6w})lQEp;4jc_D;p|Btp2 z=4SwV;xfNH@Asz)cer*9z^B+uObG9lJK^tg51w92FcDb)2Aq5>00Z^g4Mfb|bs!{> z6Q8ep1%N*PUg*36z!S(~4tmC|*cVlmE5t^2c$pn;koM5vTmDHC;Rl%|{55x{^m*PabXT!7?_tM{FQu`~f1WQRjs?`I7}=EM?$Uh+uUjO=1EZ&#pgMq7tE| zQYUl(#^feTaNVshx%F-KVEwn+0EwOd#PN*A;{Vzbf&ymgosyoUYltTa3q4sc)mB|P z#BI1YKWtiFGM+cZ?R*sZ>Svjf)@A@YS56^?v6x_>F^ltKJVvdbX_Rl6T0VO%9&Bmx z5*S`}=k4j~6v@?+He{Ez(^PoT>E?nAJ@|S(jSP?2wF+*vr2#hexsk7uxe3~mOxR_< zLOdP}U~F{?B#0*;sI# z_GbGqg5}Npg3#;Y1=A_MF0YV2WD$>PnWr?@MAOp3!~q&Af@6+dQ$<*fefCHvKwW3c zPkz&bnD3#jBaMy~4i$M6QVs<3X(dfd)gDCw!t(UFLqir8%0!S1nTxyPJ<6P5E-j2? zH1KmkeL?Ok+a_RA#K0m$2Mfced0&hZxzO3P_Q*ROeDe@Dx+HRr=$z` zZ6Y3tw+0OY77;c+@2eog8^&7DP(ljwQ@^2|udH-N@gtp|5rThp+3(%jXPl~bTe?EZ zZUn@W!BvJG}zWaue++o zUvqjF+ZSw*=)XZ7U4-KvurTP8J^ULV#xjM{Guc~jK(i+f*H6rr*K4^O0|uN(feP}} z5v|~u$bKzbW6rkaR#=RP0=4*pOkZ4nBML1oZcKrda+#f89SH>%0<}n>FAX*H&%5`U zowKbMMnjjawc}B%s-NF`4#flneo0jpiH2ro);6@krOU|T zI2~Y*cXb+w-`R`F3(=X;;a;~8qD@R&;@B7nvsT)4#w3&&6}Yt20A#=@$K5K5`>0`I}HGhpO^{X%fMNv{g69Y>rpR`O3@Rb z$_Jcw;b&QNKDah$aEsEKtBW7`N~YN{u@lJXF97UToBdxJ^5Bv9a+vaX^RfGlYt4Re?G%vyldb z-A6rX>!ZK;j+8w!t`02!pGU#*oRM9dBnS;ql-$k(u142~fsJ^-lf=P2^)KN%Xa{D- zgVRTXm+3u?KPJ2-N(}BhQ~V{|v!QXqW8mOCtw)=>l$Jq|1Xt5IjtOBW8!W(AXrQoS z*1FnNPm;a6`X$8b@LM&=ZMXw^hVe~!v*DcMQ7NhEqFNZ7)_-qZVKS};&p2*aNUc8d z;usb&(wd#J-G&W$CA@vU+Y{@80xUEMzAu;Lu8d>C{2xxQz*hQEy&-|xZnmWFZ+}>i zw>t3Nt)n9pn>jSIHMQ#Fn4qCRc7B|LB@b(QR4G!(4w7GjRMzrLuSD6P$(+@cu<;)U zk7WCgbD|R$8<1@v7X^@$L#Bwqv*L!VB!gRmW5Q{naIx{hz&uW*)v271giY5Ir03As zFg;tXVYj;%L7}>zIBEV^WF9Pxewv`3%J;3vjN<>Jg$bETl_Py4witY9qxu6)rV$9u ze6TPULM6JHM2;vKqw8Z4k^~i4sH6XaLWHzuBbG118qQf_EIUf$!=P)C+>SfqX~)71 z@#5m*Y50r9_qTBCJ1mA@Fg1Dv*iXsdiyNDB?^^6%;ro;e^E7*tE)U=qi}mm*&4kMnw7&Nvm6j1~D$^B~yO^ z+_IwAUT36lA1|6tKkE%0(Q7TGxo6F?)3tNP3Y^t?QB}Xa?k8(V5T$EawMA9{uKI;eyH-!E4ni4*cRayJ22;saR#t!ob$CDj5N9DDsMu;u&N+VqkD#yDuz#L_`?Lh{ zJt;Q%|Lp?!Lefya>@7%fjowBai2po2{hB#$g(YE4vC|YLveKD3_@>AnjAD3F(I#{@ z*JdQvFHTbb10qwaaFKKodkdTw9f(jY0NbFasW#2ty^^Hbk&EP^jKW%kfuUj_Pk&J{ zj-i1-+m@6%4-&>@(Xdvk3an0NOmd-ZVea>&IV8rQZT^P^%lgsrxJ+On?@R(3id<0K z=J#BE6diDu8EG|Jpw@NdpnGj$HN=wqL@0F&D57qSAWAIJXd!oJBHzydw@8J!yp;%p zyImCL*hdjpl7Zw%%LFeZ*_cG!fsDMENPLo0(v!F~)n=l@eKcv2Y5Z<$>4@5LCgfC} zPRzd@F5nw6^@w`@_~``Ttg>Gc>zeZ|StDjQjwC*(w%_ih2r=$)T%NtBiWLQASX&e$ zQ?Gu8RS6VK6g1>SD(w%Xa_P%YRm?=q;(=?1G|v9klKiW42Jur>Nk2cq3}mOdCpk#4 z)66@+H)WBmF{Nlukqj9_WCuqY3Jg=j9@b2%u0sdOXfVl4sWYHlQbaOXL4Q_sjp(+9 zHHy~7c_cA)gbfWtGH7%ZEDDY=$N;zY`F^uy(N0VlgBuQ9eL)y#D4d6uugo!y1rkyp zbwaXaE`rWBEm=}CZ1}SKM}=~R*(y87Q4!`PBuIGTIp_hOi@d!u5*$Aa4JlI=2AK&A zIdXYC4Sc&gqm91?1#;6{aT}>v^5+ljc%$ru!AF<3@oZ}5fT+V=q1f&vF z@4*NuX#ogF0VZC3i;fHv=zb1Df{UT#@slDj@FOCOP~3yBh`{hjy`EAV%$_u^Oe`%c zVu&Y06ijlj<*hxFr88FG81(H$n%Y1blNP4dnoubpO20qW?E449FS)Yc#B_zT_{ow? zYi!p#`YXwG);MS(0V7@-(Nc;D6nk6*ueHMhoZY567BCdlc$ULr$Jl- z1BluJ(U1^--IzJ9?N|!*)dPSGM?@Y?(2(<_5k$*AAJM&YaqZGPjT)I4r-5CX?$pX_ z7yW-UeS=@6-S_t7lWp5>vTb{^Z5t=sHQ9E}WZQO=CcCN0^*i%?-}gT_pMCDV_F9(~ zJy0NbLskjN&&|BaBKDxeWb1Q|X{)=D@bhWQ<#&0K${w(&HJN-1rdx*m28onps*uGT z=FD25U^9u49m2RW--*pZYRY4&A7l7gM~7yDaP%=3E=k1whL1yZdzvq*?^_s%FCWPH zQ=IAcT!tpaw)hO8qKn9oDL{^SctAMePQ#l1bk(&J^)-~Ol+*{IPD|fKh_4oDI1K3_ zi;q5f`}J+Dhfla#3<$LzYl9#womj}?AmWp1xoD-%Jj{pZGSQ!c&|Cr)W-L5k{SOFf z6cdij3@ckrzxra}a5ZQ-%N(-48RZnn)q0M0;mhz$(Ak*xku|Q)Lc+JT82$nAjb&H1 zoIto5779vkgPVY*yns?WGE0rBcx?g7){)({=CLn12|3V*7U^EMv6?aVMRHf!5F=Qa zd4dhe$WRMqC-B+yoGc+J&!(a!w{jG>|6RyEIH%?v{m-E(MpuV9phFj-uD1ksM)0w8^a{ z5v5o|Bq#XoBQjb*`v(owV!7%bW#ysS^;5q~3_5Y^UZ&8v3Zd7#7T>Q=qfN^YH6?yi4GQm!;7FgA{uL|vNhokm%+-Y^JrtJov((IJ zujP+QJ|E$uV|+upI7Ws-jd1#ANFFpgk#{A2fMm}$i#@^JANx$TLzA0q-0dEK=2RoO z&|qOmKYDVIJRMtY6nwJ0I#7LBo!TgM2aSOmmkaIu*GSr_*w=p$=-n?}(0n^&Cv|@6 z@!J?Jykf$GBV;qF!I7Z}I*@uhz9T9NN=;TStq=8xv$5(d-46g*WaL=3I?E{9l!Zn2 zP+eVO~FZ>@sr+o+BfQ93*#4_3_22el~6rkr^AMX4|w^y@$*oODg1m2EZtnMkh)H=8P|(_viB zrSv=#J3|YV8xY*dqNFetH^IcOzBSP)n#%~o6Fa!i>Mpl|0>>HF20tt;fX?1R(cR?P zyK+J{2L(l)DB9Dj2DYD^wI=#jaYRJ>_HDA;${~(L;fDlm&@VcMq52S*ys;+G33)!1 zonHOBeT!1U96S4g&k2OnYs=_;uh>|mdY!2Kbr#zC@-a4%Fo0M89H8|tjWcIW^D_Kd zopoRb{VK43J_f~1VOnythwC@`1%33Z`38s?BT2}n$%Q=(ZRil2UT2{mb z2HybMps*c7&R2ruub~6MG!$a;6%_ru*;SjjZ$z*SEd^ty5}t-ATAHP#5g#@6?A1%N ze}Axjts*>D8Y{k%fm<7IgB~r(`1f-U$O|C-MlcC%toMASthf8!Mmc31?Vci#eM_9Y zT9S3U_`|cD(&eY5EOhZqo89~d8oVg`CyLK0ZvG|*;1iqbShcT8LCt`39nF?);${P2 zEk%X7vEqSgpxLgpT*vw0Fw5)jumb@vK{z#~v5s`2J1SBM465_GA_;?)h=}mQe?Xxt zBye}*qQ8APFc)Fy$PGk+Dwc9IjFrdIh*6eqP%(00 z`TW5Qx#aPrym>V&0wQG!8ZZl55^cE-cCe3(6rt%QJ(v45c++l?S|pe3`*WNG9P(7d z;s`an+IpeXOemceT50J(Id))5{)nIei^!lkMtJ?mm&&fS#idk{ubuLeYtuA>q8NL~490 z%5^jif;~tNi%BUEy78i1(f7o+C4B+9! zP**1ZwX2z=B%q{|)Y_f(5XG1At+qBmPj~&`WBJ`n~$>;eBQ=-tX)}Z?)S^SE_wJm=Eh>sj*pae7}rrE5Q`n$ zLxg3u*$X?9e0sBbEm;v4BDMjor>z@r9)opI$!MktZWdsbrpz{}11KkIk~C^TQFe=A zmb2BO4!b8Z43tNRc^5>r@6Vi>xS@ss-*>9fI^sVd@`o=S% zB&$^_P&e`#=*A+suPb&21#0LxLAtrx*+Nk33euhheTm9Qq!nrobW@RYD8yaO&5M7_ zlGC8kG&~@cMwMf)MsZwQ58kP<9U{k7lvi*r*tD_im?vStz=AFbT5r(pLeB|g;FM!2 zMneb*7h-=;8TLs|P6CrsgjGGNA5e3Li2U*FY%PNEd7mX?g~-=%|1teEvB0LsR*qAD z>Cm6zyTQV^9nY5$KUjnCBJD{C{i2InWge{o2c0WeUo~gi3JS;BTCt(P!# zY30>J-;)P67mRe@VCG})fYh|euCBY(rX!6Ae4p#lY&X%PN zAdLt%4$67a1ryvp94seVPe{Y$aUwyNsMKW8?f~Zz@HIZI8N_DF4XYD|T#A zCvs$_hM?jvjY-aO5T~-FQ@0S5v8Cuwc5`OwWCx&5AHUgKkd&?GhNG~X4V5tby|su< zs*`eD1muJQBD0kvPvsxKvS_g$enbj{rY)S((S{5(Fef*d$&d84RQw=eCu#&}X^0SQ zA(bn}d@q@u-BmZyMoTRy&_p5;J=(nFG#*wJlSVr*P{U%6l9$F9!NlS8?3MvEhT$^VD6_rKkNfx0Efp5O39Dc(+-EX%-Gx3 zlu2iI15R17?x(W}{;BcS1A)M;u-E#`NaUI>+;>O8_>Ycm78Nz7DJRiyWV*a6MbdMH zZGWAxh{gJNP}oqZH1%dc-fE~&RY+O_yGu4w+kJ66-mRDf0bi8B-VOq6>`oNEE=+qz z{%{od#w8hj3GWB--goC-1t2UaS)kx3&Y`j_sV^Z z2IUI$c{)Alv>pM5fahX;#dTyEO>H1TKvFtFl3>NuM_d3801Jauf@_9Xk;SQ8xHBRy z{z-9wnpKvwo>-=cWODoqWwgoy##He4$PhoR7UvdvHB{Jqr{RnIzv>AK%KN2+yQG12 zkV%I}chR@0)8}EtjyMhYvy|@Rw_E7n)-e@_JTpG@ASZicqez$j`RKE|!t>O3m%S_4 z5YNPwje2%n{DZn69@~VtFe?9oXFFx>SetkE4;hS?zp7M!-Y{KFY$xCc!NAgoqP8}7 z?ObkDC>LfSlgY@Ahli;y7<@(T6E{IuozbGfP@B?goqNNDX(E?~WH-Z|cyq9eP7@nM zvWsGA+HQ+DX_kuO^%(k=PPY#E&6Sw&nnWWFhkFbA-*P2N>_k!9GeW%h12n*I%mdaV zVeAmJ{9TVi8tROx9Od!@LJF8O&L4nCHp<(iBm|ap`iTxGPWe1(c(iX#u&_4DVjt2} zWn!)RIq;?o26X62{QYFJRlPP!I<6P46;NTMBxe$!dD`d#l_k~5P0U0X#I8t^E6|ti z-i*u9IV)JA%sSW)MdaI!luB%$oWSq(Bq$Cpm#W;2v0f1V!~k1_#Mppv$`(byimmX9 zaD(SNFIT$hCya#dR)0yV)Qr8QF=U^uJQIfOvhuz4q*(JPosSB8iwyUxiXQ@|5sPz} z5+SubDW~TT8(Wa_)*-~F;{lQEMH*Ve>)=s!jZR#@i=M+qbvC8a*pAZ}CJ5$A?h;4{ z>(F>#epLVkQLru3ZpVuvY3gX*3XFnDjDusafHVBSi7%oPg3yV20v-?)X(b?nJ8EP* z#eHW-@S)#4o-xDI`=RE}lk5<0>7+_H@|klHAeO-6p)*6S7K&k>@`7Z`^^_=kt|dx? z0!Bm1fZ!>}NJ9`)@lgwUL@1!ZO^^s>rPFr%xDU5DU>tOKz_cDfs}lT?dv13v-ke>C z*a(d8mmQ>RCcGd|0{#epz1cEgrutppZ~S#|nB+KyT4Q%#4y(ZTp%~!7IhT={l{~~~ z$$$ruH62TcLPkiw=s1}bbYCeq&$Zh}YKcXp`z@iq-oYr*jM<^&fr5`7F$=wnG%o*$ zIzX0Qyw!y@ITIqQ!ENy$aE=XD=|NN=;iv=w1qBTzLBUw1E=GVuTNpA#JtvWfn?S)g zSmHNA&5fInfd$Z$}X78}s{| zlbIbI@1iSl9hAL#ns^E{V?vQjXFG#k#M?QAZC zMjgyq#XAUP-ujh4?kdg?p&b%fIr|AL{J=xMQ~y40RwfIBFLYP6Ps?-uUiziCob@+A6lKmoNWr90RH}$~3^f+y57s9xteXiY?yc_FhKR zp7F2UQ4L^o7%S21Cs_H@SRjafUkpiOzq^^&d2YMb=(sk+=+D)XJmR;KzcBR{!N8hw z3x#v%9O%51V4!y`2>B>3&2pHkl^$-5fpdDuA>&^?GCf>1;An9l{H}&zi&d@p=#H}# z$S_(zTZ_sKgQ|=`TpNOD&(L)A&~NQZQnrsS*`N%^ELqq$^aNA}wnRLjN4xwP>%*Pe z}s0pR}-DDgyzYBROrps?C-N+KR^>-!&T(w{^1ECUkFef1hU7r&VbN1r&7!x+q#YGgjfFH*fxa9 z0E#k`r=Nf=Zkk$_6Z;eeVuP_yIr?A67$%ch?!ssE9gHJGL}dyQks`E16iwlbsU6A| zy7yqd-L7ic9y}W0cplUscRNLfNzn#1xj%&8gc4tj{$}ot&wn>Z78fb2VkRzr(BjMg zo!_O0&&JMx;Wbn@w`+D$5bc z85pSKzV&5GVs680W!YAAmz>VW=o4a<*L#6>Cf|vb`Mq5%9XmmUEhqMbLZPo(3U>s9y}$ z<|2`|8)0dYWYO%1do9>LTy{*Rrkf7bgf>I#KWt)18Jw>8(p=>X$zmp0Jp}8a4o>K# zewK-+Sn4Xo-(ud;&$kpLf|4Kpg(O;1f{XfvF2$xEw%zoNVmS|Z9Q>K(I$%#A6jumD z*y*W2bNEhZmT!W^{QB4~@ia~!F`upaMQNSGLbzxmFXdy6%O)DM9%l1=Zf66iY9W@D zchz^+U7LdwR5@(@bUOXV`TaS~sja#iEB+?y0u2(t4ENvIEq-5mI8KsnH5)oeK13b7dQ;{;FDb6t} z2DVTQk+;*_BjZT+1^d95lynY|zEh}hP^HMP@S|)ZC zzKzRx?svdX+^p6!kGy6a@ArPV;XJ;%JHDk#CES^fB%v^k%vU9%AcbF2Hj?u#>m(>b z>vSjl!>yM96u>a`5wb0hTk($bAruRda13f&Ol8(Vgi6_LG6YX0l320?L=SP4$T4We zUxTU>^Sx$`2--Rfwe>#iY|U^t?~e-o)5w=2v-#@@$V}#%6b7>;;K*ee-gCd!6R3Z3 z9-QH-C2F$5cfWErkYw{QvBZ9?J>Y^+G9>V1#f8|76)cu4K>}Q&r{N;u$pSWgBpBda zrq?@A7}%<$EmX*Yn(wp~8h$QakB<)-D?>I=RZvN5fyKxtaF~3vqiM01qM3X?lCr}~ zod0p_)Abp#dCL-(h|iQ0RNnIgAVO7}xUz66P`E!M{QZ$@s9YmIk>FN!$rJE zWk%C~sD8#p0hv98u<~nftAhV<%EdlBY+w4jdaI-)i3baaF8x=mI=_ISVgVyxLm`~z zLlUowh=4#Y;h*HRyHt?httXuu1|wkC9k>-xfgRtBETE1{k}!S?-td}y7`XMkpjb#E z?M7u+Cu05GTMS$aI{Ohoc@H^e8B3fI-Mjo)yzUsn{!`qVQURe(J!H0XHvnXl22jw0 zBS>jFL%~o8_D?-Q`toikWGWdhD{;q1>uAQrke*X}$wZd13X-PW!m5l=pLk%A6nI&W zphgz-O^JBG#+%Bq{GxV+VuKm~h^u|q?k$GG$BCF5IIPZKS=8@=Abxsg;(tpJqhXde zPZ#VMZ`ytw5`4SLvT4;5HQuwm&Ih!F-#pp6+iX|n7!smWmtIXfPAS`cT;pyUK*9 z>rx#O@1)iYE3p#I%YxQCGD8}=K1LDQW@M@h2Bf&sHCO1$89NE z8>>NJ>9$$hjLU3Oh0Ve)B%N3E~t;I)_uB>&yQN7VK@ zw_$kt3$5<&@wM)A+M(?vnS*gN99dQSX)(!88coS)gmGspdON~*squIgFG40o;OX^< z^WH0N13w%-X~S*fl zSw~3xv_3;VFc869KNKs0`*h$6vrOBJ)9f@~;#O1nz%EHuu#!7Lo;ruSo{6d~D3$C+ z$%)`G$RIv@}cY7YSLl*blCUd`rjhKAPgIn58ro2j}k5a9LUcjC>RP)|+^ zH?}V4YPVI04?_-#3R*Cb^6+CuzbXb716puZ;`{AfNlo7{u)O0)_s_*BGD9v-T^kCi zLwYaI2dsyZxV$o2Q(8+v%k5ix{lwZmd0W(YnMBexlK&xt| z{8$4gJTdkgxEG~kURXHsC^@Ijai0aU+z-t6#+5-LI!96ZW4|yA1n1xD5uwYp8B^)! zCyE&FNo{J#558)}k7fvRgiX2_p!>Y-YfVbyCNU{CkL%I;u$~5!T|{nmkb_bo@)|6${Wyt6MW($rvR&d^x1B}TjEy-0AdohNv&yNnuMKK}3yPl4ID0H}~hR*9;Xzj-8i zJIisWluRY%Z0QOcP6@eEoQ*s?@3-CX!yTefQkvc2oyeP|G1nVWF+Fu zVijT%1 zldbunP{Dpbf+VdZvL2-pjjFt`^9R3g1LdwFfki2?u&)$Ub|cOUGB<5`KvE_?i(kh@ zF|*oIBLH#p`wnl#&Zu*wvwe^1Bp>~F0n#1+)*aaf420Frjh!-)ZL*a+1HGZF^FGs?H~`;x@l3Bw5BkR5iMC z*Zx$0jJzk@iJV5!L~N*nB-MbQs{muvlwyr_<;#I*84A|HyyhBhTsEoJ(hY{4(Q#Y- z&2pd57LQes8B>nxSXLDCQ0feELBEPQ$RIFNw*ZF`fB~+0F(z{`BuaN(-fPQXGf))e z?PX#KAMlV6nwe=W0hEsl&E$B5zKOA&bYAzO_gnskYQh22_I(oGLmCh{wKM#jk*dGLBgUcKjnKrh$78+0%Bmpxwxz+sK*HX5*f@h zs3(wvT&iWOd@kL=H}A?|T?M@jfsjx1*Y)XW8%@6~>-zfMDj9>cN5aOBo84bnEsb!5p;)>O{Ux}`0w9Ey| z?{6R~rf)9hUz4{U6*M(0dQ34_oo4ZErhH= z3fTI!yDjcDV$&AYSa)9KVH2Awc*Ad}knv^}PsCPJGxg)?d9}!8OWf}*N&iXDi%k|c zrNJYR&x7MJn5mY=qLBO67SSm=`8=_z?q^M=bpJrum$0aDiPNX?h$CZPd+J{o@w=Ul zD<-9*v04t1`_q=G;Ign{Y$E8WjTMmCcSw1+zvkpM7{P(gmmYn(ivzZ-Z zNH6m7!x*xv1iV1z7@xAzybcqIBU+i1?*MCDl4544wI1h5dGm~p^HW~IZ|Ymx(l_fR zG@FLkx*?l^ZA8RR<4C349Rb5$^1DA4QPI#s6g(Z(+lhFrq7+-m9i*c$ZyiC<8bsvB ze+$p_0W_q+V(D}#O7><`{k9P|aqT=-muv^80s^2sFSZgz9Uzwv`|Y!#g9cdtFC=0? zR!V$V_=^+6=^$2_PZnr<5?SIy=^#<0?IsxOvd2& z6C@QYN}bBj5^>?H*eOOy_JDCmR1Ci!FgF!^cixbYd+Ky33Nr+_eAw{MYz!>%^@nA{ z3$DL=J;rolyto;OXZv9ClP>|#+@Dbps9Y4Ndg!se_r7e5w_~lXi`S9JwZ_YMF9ahQ z=zK2!pag^_pMUap5^q$J(iYGL=AjPVt>;GpuiN}%V{;8oY7-&6ZU`?gPnf*z`It+A zOE00UQ_!64JXybL!fA?mA)+bl;P_jQ>F;X4wQ&m*r}>*5Nj(7s67og6gjelm{QJZ9 z(?Kix9D1vfstTU0{%&NmN%m zIio+RZ|d^QlLHsGmGY%rYEYh|(oeJb_M=R?_;uc1#DXfz;TuTISJe*#7cTq1YQd&v zW+ZrU@mJ0%@WiMS=0(NxrN|R=0AqaA;C}w}F*VQNP9^QTr3FUP%cHo>i5NttOG=5N zku~|s$_1Iir%Lc}qy3Dyn()stO0Wa$e>@tKYL61uf?yS9Cs_K?!4# zr!5sSfr}E0O#SC%1b}YMSdL=^9dg~zjc^vMDOSCtUr{l*bPRsi=*{!na)zouzK;Z` zYnJQ^38_a_XJix>QwI%h+uqblA@4Q)k+rgJpc_u=KS|(6Lo$TL%GEb+ac9wIt~Dt_ z;hk^XC}Lr&^Qry^VhjOjYGK+pjyYG2J<1?5 zge2$yyAXC|3!6V*t6Xk>+Sfjf72)1sD2dMYJjnZIPCN_y#m&00QxVV8{HXtcCph(< zs{aKM=!kD;M#8Iup%4;737-Ta$LPu!O4~azoJz?*7ZUcqR4X{z!|pc?LrDv?A-OAk zFy~}R(9BGQ4_^hvz1Q*v5^?%a?-~+q$JDm!x4kFRRvn3E`fpb3UEGFMWWgnRN0DwG z2ulRRpx@<8kTq6vR`_YPf76TcD7&3|w}ni`2lLh{#dGc5iO^SQp-EJU87jOQh8h)`eMYsF>dv+HC2WUWPV{(~^$mOH;FsxP(WzRFKUjxePOH^lGn4m52CPd@>et7g z^<2Iu}s%0|6)*_RZ6EDanmKA$vWMJ`hg0JTb4XAOwSqqmOA!{)nFL9EJ`@ zpD?T)#sDf0^P=Ym+7N%|1E*`7nK@hXmlVa8d9VAYND_twW;N;9S2)`C8?HEt@NT~r zWu<^ohe2WHJ2BW&Ho$6G3{kIjdJN8bx_U2DOjT5rxGQ1nq624oEE<+`L{Ul}G?Rzo zn7?i%{DKt`xk0(?VYwe+OOyrc7n$yDqlyxy0~eL7BE%&^RaR7xMmbCr@V2SL}i_um`Q&56}rTAY}(IhPx!=YV^`G&82)v_zVK5WPhM2z57 z2JzLzIyzBJb`}t1BG?1;lBT4scb(jmQI53?qg;7HkuSmWX6lSZ#LmLQ;4~;eRW`$p zDB@dIO}*y*9GIx%O_&%-L$)J}6=xRqK5d_}ykv+&uhV^$hetXQQL0cJ2;$;*{gdqv zw1qB(Ox48bH*IV`O)fpPbQ0e^a|SHSu;slFfv@NmQgCO6z6g0|t4`Na<>Zq2XPx;Q zw2i6K?z+zU&b%~OdxMjcjnC+6&fZ`R-5r>qY)qlBsz|glNcR7rHjzFjflAxLucSC7 zli?}O5?N>Zgab;1h-v%h(tIHTehIkw!iD`6`H4=}JU*W9^suPhqF0ELx|!BGlvlip znv%Z)St1>vzfQW{DlW(kSB%aw-W$GP*@k{ke`1tnd(Ig%x1uRph-eA@3fAHo4btR& z@<+K<)zJlrKG-^C3ANH!1r_|KN%J5j8B&8==J15MeRp?%y1PUBy{W@o_!A3$vUzTZ z0hLfdkQ=x7!a3`iwSSPqiT17S)aQ2*S_PR9;@2JkZ&yt~j^s26R4~x<;|oNgX%Cb5 z3*XaroOYtMm>5GkMV(AjJ^+P`n}#gA?}~%lw(wUnHM7X@GJ^3vJ!baeu|AU0^7BN8 zQ4x!X^%sHVZv_M;FB?f6d!+Z5+d*IGhoj+#f_k&TC19XK4A!*G>dY5ooW9zPer|E2 zBVRcTvuB@0LEN0um+j^G5MOy&!GBfb>~eI$*~$AA=9oXa@2Z|6^ENWCCb5qP(s}G| zi3#iR4I}~nt~ApWhutc8j8L&i_B&}1oGOsIQa_-p`-R{?*W{xVqxBha%?c3A{-7uN zD>!W3Q^J9${IkWr9I`UMy$qOQO2q0-q_navElIT)bJAIY_9vY%U`;~Wh&bvTpU#QE zmCoImS8{J^+i<)2-y0c@f|8yy;nmgCiR)eBeVvsSA^eA)g=ZltJvm`X9h(T`^=hBS zrHEI#e-dk<5Jx2)PPG})gu?|j5$Z#1xhs*<)M2qkLMAl^7EKGYLcZYDRM`DzC~$WZCBRrE9>$=Y;n}zAGP+@7$W3PLBnq4To@|LVvZKlQlB* zWBgU|Xe!x`kLx{rqWvD(gaQ1HOtPT~|Ls)W3w#?uA+p1?Uvb>B`aV-4NmQ5O6urLx#z_v zA~jG<08vnGzD`mV=zLh$v}Q)p=|X<7+&$j_~w|i4`V$BC93sM_VpJI9E{2ovD>T7q+n> zvkUVJPEHs)>R!)z6<=d>DJ1)71}o*`moj-KiK?_8(DCGG-=@|g6g9$lHd^o@+&6Cv zpRo_7A=;l#**kCE@v(0@a-U{kEDnb46})!PBW|;@bi6+AF@ZM1Hb{0+1*!{QVc^aqCXpsH^?MmYTolaY*f7}eB zBVb@&n(~#|xFl?z^ZS-v`%jl^37sK(QgqGw^TF1P#lsW_k*0H&uD_Z3Wu#r1bMu(} zFN}h16*%ns53w+hkvHtW;SvaIKvw-&JKoOXiI%z2u8ubS^Y`8KA7Y=Nehs251fD=-x5ue)c zd1jyyl)yE+u*#Cqv&(FSkepKz^|RL@=)}|y3Fb$Dt&_HKpAuZa6?v0DK{X#0GjAZS zewp6Fkl(m$fBIb~PWrdaT!)^}bGowS^5 z!)oU}5D|2-AJq#5{V!nRpa1sM%^*PnTcUzcN^Fys)5G2MRT#QG>FXHehNr-}n)$@r zyNNG*h-xe)_9Y}vGe_iw*`!{xsO~2du1Cs`kBy&!&Nn|DMB4JhR%uWG5ROgU))LZ% ziKM{%B~2P84fevBZa|B@Iq_BmZUh5Eo0;(udNp4JD;QKmS=oRLV(+*aqBu;#cH5gk}WXj?BYFSAI8AP_6lK+eS;11&c~ZAEJA)t2m4 zyVYD=3rE1PI=-E;Z%gYy&uMo{PQMr}gUOUuC|v_@x{k)niaTg)`${tvfHVr6FoQ?K|$&C`Mk2EsLjY)nW(jgm+t zt+$x*PKuTb!W^Vg?+}+bqkWUt!O&kD=LASSe_f90{1xF4ykkUw*kT=bt;1zbP|YTW3oX$s`%MnHXlG z@G3SeuG=li2zHiK3Ccme<`v+f4tT+?7+}UojOydiq9Oz~#bHU;Ksd8>*(*`%_%4aj zFC_*{Jz}IC^sJaRoS1;DN^82r=S+cz*=9(vyXBFVK3}Fum+LToy!{Ydq>hsACtaZf ztSK5Mx7f;tg0gzFxms4yhBiah1UuD_W)w!boCjurID{-4KV zU+3XrYguh+mW$EEO>u0?-`{4kxOYZCTyArJIRg=zpFNhRmIFZx6~c@jIU*<{g1oT! z=YOV{m@n2jR<9Ik)Rv~dOOtb)_3ghAPyEqi6Dn>L2nwC{KlvM$CQPb}K3`zjRCL&K zyZ*FMCA)Rs=h1a{`b?AQg1F)FnZ_jHCZxJCkkam!=ey=#8Rd3JHrGhV==g>_NDQ?fKpgPA4jMwKP50zhK+`wi_!V@g0Y6Z0RT^Z0f^^Q zMW8hgPR{NxAO9%WquOuxcyWF05kV~BWHfHlFUIx6L>-xsSvu2ml*Gu$drM;+O&%dU zqV!~^%FZ5GVF>u!q+;YffurXt^-4&PW*;mVYR=HvhT)*h9I~3JNX){?^OV_#{Q+~L z!*B|6;#xat)l`Lc{##VNcMD=p`$u|Gks~6BN1hK6Qe%|Gq$9|6Igs~A`;_t@nZzJ0 zEZu@)iI4l?_OkNxp{WPVA3RkeScj&@?dzHG+pfUu<-i9gGZuEaiAy@7N0}g|zISTC zt1lAN&G~YKl?Kg#{0NiZVN&7cU_n{9ix;N-frh30|j;%t#?NQzEhttPvps zi@*z-nW${-;B^A#HH_QE$E?_}@qLR2tt_V@vi^h%#hBn}R3-EB6{m&0R^Qjo`PDyc z?&6<)PZJtX*XQYbh8c$>vxH%T-$Lj1ELnuA5qh83%-jAX;rZ?DO}9PfV-3pqk+Ef2 zMUljpeJlWS0AT26o48~0*ktTGDw-S^;6oX%zyopGzTW_b_L^DrT1p97^j|5*gapP>&BhF) zyWUjx{xrVqXL^E=KNZe@AN|R(Ydd>6IKV+4;Q#KGs&mFNXE?f4Mk+Bj+&aeVipJ@l zx<9Ez5RyOSsuYOw&L7~nZkHKCc=LBDHHqgw@}%r$ag_S!6J@)K$zbv zC`G}94RK0hrTVe}DpKhx;^;c6=c9 z*^U$#(2YlI_%-Q)Y^(v0Z#sn@TD~Cnk5fRD3idQ!U_l*H_L4BocIE ziSw5<7-_;5Fc4Yn&oi*gyfds)xf-9+7LXXl3g9+VfB~;bs|W!68|A5+KEqwRL$jf| zxNgT-=o^3N3^L`hV+6FE&|P7_oWB`{@88!(k8W&1stOiR_|>t48Gi(I6NPmC0>jV1 z=eo|NWKfjFKWhXiZw}ZOg;mPE843ku{WKje>GyE`>bzN_x5ZD5Jt?Pq@*Is@}N!9iZcKulz%Tim|_?o71Mv&=nkHGIFXTBdI9}!!t2tiKJd}6jvHU zjwuI}EvbsSga}&@S)Nr&=KqXiO0xEv&1sisAi=ei^mymfY82e8`*u<-;&1*N)_70* zXmhBIRre>)=!G#AZ@y6UJqH|0EGfD!eLiG!y$>sr5EDjIeanz%5wecf4vwlOP+{{; z9ZhCJv!iEzLeNiM)Gvqs$_xt1UIiufKQ+J%FXRx^sARk+mMLPu=ZnY|FCVh!4u49e z0OQm8ADA2Bz+Z84<>tnUx6?E<_7M;@?nbk~%6jO!Sn!K-uKr`gs^L{l^CAwO2uW?UA* zY_{)%i25i_X>^o1GYI)tGcJ@4t@hVYTI0~~)-oNwl`yFwQ?eKpSd~6QgI8NQ@+pED z5_?LPNzYR>A*DI-R~t5ysUQnuP<&~~Xh{tMG+U=T$sZM*+;<`91I*vL2*x7qq_m28 z17CF9pO2kXD~A#AHa(ylfAU67zlkT;cB{6G`n3~AnyO&CCe@IoVU@9ua#=K zx341+J({PR8jf3P-Dcweo42JMGhui&Y`Q8xu-xn@eh|J>5oDTn7&#>Lj5k^mPU#?V zUuqEty!V@&#dRZ66yN+PVNfYbY< zPEpeXp-~bpGzoW%AaFjOU3~9~Ar-w#l7t-*S%POB!R*JG@X-N@2WLH&u}F>hNGyuw zKpbJ~;H*WF`f69+-ch$&$goqigQmy7n2bYH{DOpVqn(j+-A&2RC}N<7T77fo`}MZ^oGEmP$6@zD zsE-QlyF*W4mZoD_G6J?I#nKzJ*uQ*b00r2~Vwcd^Y3nwEYs4Jy32AYe_$p9?mMJK& z`r4>T-sv|IB}wkelaR=#vjjib*Iwk^im3usY%0b&1G_RLxh0L)MBi)1ri!SxK!Z0r zLTTZ;0DAGVe>^7mSzeEXb|MTAIW!sLsrCoEL69bGLRur!{q4nxiJ#85pxN+zV+N`c zpEgDby%hO|O$xsJ=zJ*>9MiY$AUM^(FVG~kHHSGK**ciYYO}QY>|)Zp*6j)_mCltg z?l;C7=SmPXphJYVdO-mlBFH6yQbdh+`={FFmVOeXQGK0$x_m9$AHl^QICC6elsh6F ztWmG8Tw~vqB5c~V98-_(xl5W zIo$ESDcN>9n)St}@1HZdxZjxQeo$6#+a_2XP18RHTyOin@JwM~Mh~69^>2rNa-I=^ zB)$aTMdVmVf&_6zocdObBBUZ!jpaxFhU^jY-_^}DgQqkxj*eK>vg*e8_Z{v?Jfv9< zsbS#-m`D=fZFfr`p#_Pj6wNGaLi9QRaLd?k9LiKN$e>w{D7*zT6Om=bJ{h*tMm-2K zpx-Hd2f)Y*D;u__&DId4`eX>GzE&!|U9NGhv)ziBC2_!;NTS2dA7a7Z z_KGKOmdibMx5aq0H4P#!nJM<$0 znOp=HL+CKPH*2ru^Y#(Jk@1H#F24uV{?HW#QSf~37UN~AJLg^A}6n-tE4+nga z!cIcxty;mt+KG1S7voAj9AtUdNp_4wcB)7CUM<9RI}vkZpq$ZfZ1CX0WCS2LXcnD|Scn<1gq!l8LL z--P7*=1jo9FX2xeL&w9Q%}+_J2B+UvuoQRczHN1ScknDgwB>&4rq^0SK)CsV+%tm* z2R~JrxAJS!Tgo6MsmvhCN*c9@3@h%ez*+(pZb&g;`M(9h;XzZd=cLJzo5H!@2;+@UH5gvu$@Ls2w)PP<|JhhXsT1Am-NzW zWLevp{ZgnLuEUSyB&YsnFU1S&lp9mA=U1U45S~i1A~+Db&tA=T7iyZm{>mnmv)K;j zlAyD6ew?X-pfE9RZDNyyb=wwo1-sQ)2kI^&zA6W$NtuY5^(HoI%>_movWOv^*rF8vW1$Y{{`o&C4vhao`s~8y zRG+1?26!^iXfkijNdMi#5yc!ZPfw&}O2T-iH6?5oDFe?I(`+tV*{Z83fq~(XU{!k0 zIk+;e5dkf$6|1j0{ne^_;la54y7o)ymb*(MUA@4q&zZ%O-ZMx`0bFqe7Ycq1YlA8f z!e~B8ZspVNp|M#PGA0pBMmj4dn};ihDP_H9Xg=($>M1;)xMPFG5I4$Jwi1e6(4GR+ zsZaWLdk2(REsn~F^fy^rnj1lZ=OXJIjS$tuSNVLFhFpb_Amdq-NA%7hVb8`f4ytAR zcN#U(Xx-je=>besS4z8^;HyvHR~EJh`w)3YMzPM#k+3n#@?VK@*gyIV4&eDdZ3nOv z-O?1z6wxG^!dk^ffkxI&fr(jHkK!8=GS-NVxVDIY#19M?2M|H?W?OD^3?od?-w|1_ zjvk=5eC2fAbJ;H)@5|MP?CG>KPa{6}jNg4iV%M3LB0J)*_pTo?d4VDq_aI?;reH4) zc1_-QkMg_7`kN$E_WKCGE_bMiiM%f%ibrhy?)=IY@%B4dUuwD&%ryCU2k!gcKfyje zy9cN4{_W4c0tKp&Fg7u!Iu_5rm0+y?VCh?{$xiny?OJt;6Y@BAx)tHjif;>-|A~OG zt7XzyPk$7E4pgfFJk;6_INb|+d5Z9U+gyrh`XEkw zbl>_YZ02ifIpKXD9?`rUC3uktAl_=X?D~Z2e&2R)aA3!!AI`L0%edu>SRJx}4auJ# zuaP}>5C^ZPmvw(CxeDZOe)b@%d0eeQKqkh2D^p0;P5sWig7219*e<@piEQU$zUMi; zuzgI3URA~@E|-BmdjV0jpOH$s6xRGuKeL_@dgTv`t!zjueVM?!t-_iM4QKd?i53Ma>>2P;NF50abFl1YCl32i#6sR20Fjhi1xWm@~B6@Me{f(~Nqh!|OjWLlM7bd)=L6_PS2t zyIMq_lFVEU29rXWpJdzn2mfVq!Kukr1aZ1zAszZ+WCtPh=>(|Y<7Q>CdWbpi$ywiA z@0}vWnxgc_-Sr^pCd16>9?$ld^!zLt?)wjb z6?m^-fs?pAKqF!enwWwrcfbzqO`Q!biz8(YXA;HTW}NU*r-I=6+pF!wRO4=+rApGG zAD2E1%h~b3<^^q+*E^N2z;f{VjF0oBCtP@o2`<({7MOTMYvHC4NU(G!13H}Yn~v@0 z-+)}%1t1+U;~;xt{B6G72*t0Ya4<6`mm9zfOfO|(()VI$J9oPNg#IFu!e0S!1WMkYKc4sxB~a%dC>VyQo|-8=zQ3Ua zli22O{$PKfi9Z5bBmB%sWZ!r0ehIvQ_B4w7ixai6{?&@otHxWJ?R0klhEXUc&mBkG z=fCWN+wBQN=`KQ0}a|}#a8R6K7Ca^}< zab=fe%=p+!A|{T7&8Dc8Wl_W78%ZCVc>d5MS9m0NJJ z^YVIZ$o&InGw2AZ76eB|@?R8E@R;GAhp7cc))CVrDl~aTXowE8=ypZ8kt>r9+hX?| zkA)AeuX>^aS*=72iPud8#KCL3?FJKc$?bG+ePSeRCkH|8jz$%&ji5DVI5isBT#u7? zu(2@JPD@2x4?qwyh@B86fE3{gCG95jO;Wy!h7zS96n>97olU9_3SQYQGrFfeYQ4yp zG?>ARbt7>QusSyZI?q{X+=_uhd zxynAMeq?6=7;)*E8KLe`?7``@`y9~IPhG=9txx> zey)Xyn_{JEzKzP)UN`vQxUNW-pCG|NM!15OegP+Kc(tBAITS3Cuk~`h zU->}ABw$FJKC$)?MQuleZMm<*2U9fxJ#pMVq9MRCMg*+XO!o2rQsnueyv`(tiub3I&F)G}N?Dd49n9fg*r_{Kjr-aKbsoADKgE}N zMuny+LLKaHW5$-39E%dT+AC_JfrqlhpZeIIb1YTqo&<8Up6R;hPNW_P<^7ew-(Q8= zZ@7@o7RyU|B0L__t{t6Rs+^GXhbJVD;W?hr-%uQgJ#vN&=MWUUB(4oeMLL7Jc;V`!w@j{Ff3{FF&!Xus9 zwFtWoUqLhux7sHqc%OE5@#;XTF86zR#?*LpZoR}}+?A_Nyoa4+PMi|Z+@i&>@kHZ= z=?`NwL={eeM+CRn!VbB>_lQ*)a-l-JxlhXoQ3fz57UyP$5{M>d_T70~txUJs>awZV zBO?QF6*ePCm)bSjjQkRrj-rxptO21F?M0YJ@Zddh2Oaya{SgR0!-%_2K1;65&qR0I zS_{{+Kz(vbk=jG-aFu~QP#7apfo;}e%x_>s;LYKkjb6oZri+L9ROj!F9GrjhXL^(e&6?Rxaq zW;CLp1;wU~i*TKOl2&Rk!B#yTv49bK(59S$_HRI|9>#t9(CL9cFK8IGS@tq|owoj# z>vvRIlcBtBp>7ni%&fq*L%eOdnD2uOe1ue7N`n9t^^}1+AV2_LW==L%JoSkP|D zK)KN*hg9Uyi>-5U+u!w|KW z!azKNo=$Y|^(6K_5sB0l^!` zzsHhwmtlE&Sf7hzC?!WA%$~8=`48*s!O|eDInW_s%aAk6X_rA%EJ(J`5_|n|P9o`X z*LX42YZ*&E=Z8RglR!|#I+6*FV(4sgpfpc~73M(TZ15DvZ)8dhg&pEwlebaU3BhePrTb1hILt3m>6x%ycSgt&0HRe6uFHA|>eN1Bne2 z0I>&cB2p&@Vo!1t@reO3%-SO&24vF08aH@==&;}4J8zYhkNQ6OHp6fEu0;@$Rt;|u z{k|$DL>K(qLt}GkpXWB41>%)mIec%3rTo`*L}~>ygbh`PW~x%4L(4%0pGcU5;NZ_T z1a;+v$zcDPmO-mk<||-$zMg$)7z{QtenUIGwuF&{gzAfxTf&t1_9WFsME&Ii!D2Ta z|P5vN}k$;nlnXF^)1kb^X{z?q2&|1j7vx3g&Lk|wyXPn_&CM3%3 zzsJlLFwYXgCt)K<3l!IgCXdnb(ezbPZ&H#D(MB$m^@WnAe;ep3^0pgRk3AB<4Lo<& zZHj!X#td$7oNtUXIw#Z~93&>HxHP_{LJ+JYs_AS{QT!qaag`#g@;;>vJnwJOJqF~lb%aBf$_7?yWnYY? zR15xc`brRPPffU(#1bNLQ2c4v786-dPBl~4O#^>;QLr!#xgcDyc>2GZwh$rqud!VL zyxoH9-cEI84;6R{4*r{|0PLr#9F5=*CCro8eJ$O1@rPao6T%UmRNX}EME1qlzY013 zd3y4J^V^tVV+*Wh$~OEVO9jwf3Zc4Qe4if`3;5!RL=p8H{F$s8uM|~@ACC?7k*y)S zD#wU?1Zh#2)U@#0QEtYZsL9aJR}a1=LmSQ00VK>E=N?d8Qi6`_60{B}{oMsgKX3`t ze6k2s8)89058^pGlpr32u)&#;n=5cJkgXV<9=lK?t7I_NxQXw>$`uCZA0W!s`{st$ z-e0=@6Rz0&nUT;x&L1Mj@qa>cKgnEfn1MiHHnP?VNw2pMShr5f06QT)FG$d(*=-V~WQR{|}Snw44`+E`3y(_IVz zU}PymzI9%dJ98+~nXT}VUYC9Z)WS`I4VI5&Ig=7d^(}(h^cz7si{OM|krk7R_EkAL zQ?CG@j{UTpR4nHnArJClt&1oGt`uofY}zy8u1l^S`qUHZ0^R~bCKr@SQd+697-IwO zPugWL*H{tlD`yZdEE#p~%w%*~(^{^`~(fU+4YPB`OC{;5Y? zGbpu=qhv(dM^hvQ>(Oma4jKD!_x;2g{IN;)_?g9o>I4K~5u%iZ1la$3$PfhK%8-qdCM*GB{fAj!Vgw<>34ShC~JBpgo7YTQ6?)wj?ifE8_vXtPO1Kw zt}3M;3M+FV)5iS`E#(4G=L15%a!}(vthZs1z3jP|@uWqdSHFVl(7JMJSThPc~atfZ}$*D)r-A7T5aq zqf5t+Tx|WWR|++zY-Fbr%(zV6E4Tt$#U_g5YAJrvX1E(*!&h1}s)9ic`V&n`bn)llw6!IcLAWZZ1QgxdlmL5#(nTtsfJF2YM4N+DC|HO8Ss zS%@gEHEpW=Py@9xO&z!wSnCfifQN>>7YhC(@nJ(^Vofwq5nCbe=8&!-;W5ztdwecU z%_0k+{2j%^Xwgf>-_nJxDR9O7Zn-M1E9gt%^lR-XTihC=y4gySnak@PioZS&m(2GJ zLj%{`P}!ZjX`yjjs$Jl5VZy7TR41y>hKLDX@>wWyzb}jqJ2On8e}q*c7Hk8Y>qA1J z=V=)o#y-_6A{WRwNuhl=Sdn?)cYpEGO>#gOAq3v49O%h4FDMy6a!v`=(ig(~?ZWrD zAd2Z32(UI%rX6*-)us8tjMHVmo^*!?PD~^|-p|A4ake$stP#p+5)V$R^ZuwacJJ=X z81kpvgx@(rvNeYKRDAXSgtzuPXxL!iEuj8A|8bEwN4bd+>VO!`+pt)B0#rikouofe z-(E#Wvh+40o9*>Br(c~HyHKPs%M2{`)oEQtzUd*@S`IeuEAo4M zhpi)#Of(ye{f;clV{I-9v~7Q^yDp^@_K2?8Ts%?I|7uvM3taw zuhe-a=vHm%+9^nuFPs{m5sEQCV$h~%v!ApBY-+jnbZ3-E_*JR-9@B8&u)(bS=MS!P zao1D9i88H-%Wteim2qCgCopYC$-46dqF+;@IK#DDX>?s>$mtzTs1X0O5Zi}ZVOXt2 zTe#xolV7C%5uRfrjJEgE^=YkKjF0R{yAq2v#z-th8;5?wc{eaoERi0{(P1uz@QYN)X0xXWx)^MWx1ukWYq zRxlxi;N%{Kv+wy8Y{U^I9OA7`}Qpnabovu`LC3Y)349)=+Y!3-T!&>1cv%m5J4g#(Q}q<_#v>9qE7h8 z1VhbJNSQjYXDFps<$-F9;j90F>3jZQPh+rlHo8Wz`=ee1He8y(lcR~dtvO?p3p2vZ zO`sqY;i2nvI+tRiWoBT@6O8;b3B$Jk4K_GfiQekcLq;}+tRn#F?t%&u`us!br0U&Y zyZ!=gQ`15pG}@H^Op#OO^c?hmdwt)xN+ssG zwEP7oYfK2c9E&>Qy72w!6AhVY*{k5Go=%sDCLojO!f%bf_4gs&vMxk)>!kd|4P{7D#&rmQaR_rFB@nOh9fI2R(7 zfS_<~WkmyOmB&#Z$G`G?*Pdpc2Lhce)8DswCA3Pm9zhx`FQC2@HA8Pb1kEqHNOree z0~9`~jCWbd?S;^p><}leV7)bYY<2*6qZM){xzqMeu*I0FJS_5{M^ES;>^Z2W5-v)9 zAojf>i+qVHKcY~>Chvw_bdMzvcz2~HE^m=%vAxe-i?(9nf#>bvqzQvc0@?7=cZ4LK z)btJMC$*77qg_y@8;P%-ky2Hf*~8)JA72X}K_B~DDjv*$!Gn;EDFeOt1PZg+LL8zP z7$tQBiO`r*g!aDFZRdP;z;M-pM@990K^Drtc2q&#)>258MUUD9!#;HsTtznNoq0{g zVOXc}fxZ~Vc4cjr_qImNFLu_2xH(nOhHdoPOf5!cn9P~nklOq|^#n%ifIO)@ifD+_ z-|Yq}=N(xKMv#0bZEv;`J*b$BVzi(xd_?yM!jdwJ&B2Q~y#;rdCLh{D8(uS0b1YJk z?F;WBNn@zL@{_LK*6(4v9>HVHO>fA}gb`;Lks7)!Vp9g3)gJl#Rk+6!Hp-|JIOatV zXe|#UmR4;V4<`t!%nRd*sYaD5w@xLhDA}th&2Rz~p=9m;Z{00yhhP!}Edx!j0kwZV zXKX(Ej4Z}P;74Wg7_G4(nZ{HJD*0L84&G-pl8TV>b8e4O`;hf{c3gR`x7)T*E7cwxOBIxpU5{risMgGsV08U^)Xz*UCp>iXh7W>WvS!%S6+ssPW zr$Hb}anpNA4Tj|5&n?5Hii{b{ED=LPv^oYk?SwqixTfqTMPtEhL!^z|ORoj=ZJ-PU*NgG9gF~ zaWueGT9~BYdRL0@-<^Ep*WB>`!o)DNA2}9lteBha{ZNzch=%T#M<<;^H6&59Br@tHouPlaj1@hfD}UJ84d)2`x9I3| zX8tV||66(*W2>j&qu&ou>=Yzas+71GlbI-MZ*zF0jR;|LJs;lH!VpaKob)Nkqi%&( zv?G}(rA9mssjmtVl`^QX^lY-)V!abi612`?S06_djO5^^V68Lvz19#XR3C)-x=~Oy z9z&0z2Ck3Q8+pgK zzh{L*5oC1U2DbmFL}Aytpa5dLo8#yCo(TyW#~tj z!!-j{p2hoGFAiO3QsY#Dp7+M4N*+!mGPl@MUm; zs}{Rw@5jiCD>`dGxw|^xU{`U?Afi}(xMkq@hk)A%pWl=hoNMs$3jzk!i{oISx)dIA zGt2=yG3{R=^#QJxjDw5yN8|by!r_a^QXM!&Dp;f9sYMSEX`ZBH$dsdQ!LZb~4#y<> z&c+e_xDTL}=@^q1?C@B4)*SfR8oXKQ*Z?TiFxejVdU!gg&bdCB;o!7GzKx7Odfo?~ zC2yb?WPqk_00yidO_!kaJBN~)$fxJe&)28yi~(Ns_@u1Vg_2S9*@n89r|$aDYG^^;uD_TmXHBY!oD#8{n zY}a{!YY1Agj~e1E~(>8+3Y~iPCqM_2$s~R@QgqS^uoHDz*5=ouh|N49tU@bkMG3T*=s5>MD%r6oI?;5 znZoDZx9tcGWCp_Du|HFQS(vKN9^_@EPV_Y0?kpQ&M~4hBJh0-!Ov1r$T!jl_zjT5Jn@bI&A$LePa`m?onz`|No1<(y{yOur-hHQq74~YvXJfe z;J%r`v|WK}e$)EQ59GQ(=+|0oR)#4uh{~-XxbAQ`6PNg}YEN_}7-F)kAaQh^ekAR8 zF@FB1fm(2L$q?Wukv{QjA|i3q`A(tf*0o3y=d{p7i&lDBiKi{{9o+X2`^XbeHsl2o zYXkj^o&b9(G@2$IU#PyCv912N>tFRMfri)d;u+zAK<3od&RnEn8{%6FXfg7DeBTR{ zQ=Wbdx;=B|9CHBK`df;R%CCu!7@$CWne;PV6277a_meX>n_Ef$x%=?i>Zym>Y$)_Q$(aU;@2wqbu(y!R#LnMR)f z%VO;Vo&2|>A<7)Bs5ReuRM$5T=q~p+FEzl5<*LCF&Cg@uNU|@Ts*!gq@#EhG zYYS`in=3QHfLJ7l%`=q>pF@z(>#NV}$=>aDb`8qL54)OgB53X6MV?fge}{$73f?8% zT;55?%{HP^o!BO*<#x^l1MG*tQK>iKX828Ilf_u0&q&RcIO}sjQ9}6}q+Y91(1yXP z?qV;}=x(|^y^1r3*NYv8gd z+;4_-u%lklPYZ=AI7bVmPy=F14V!$26BqgAV4hFOFv`fZm4qxcA(#*Mx5LXE|9YDv z8*@%}psn-V?b41rHvkh7-N%BtDhpTA z;@`Z5&{h0Nq;^@-Tne4KfxO4;i`+AFb;5(Pp3t69SsQDbnf|@%cC5&%6qG6RK;{DWe>4OvkWGOBgna3l-1yk7iC z{S$>ABjY4T%z*R;$)jFOINGIt7;hQXEqhomE(@gKJ zcxcKW1MX)J-EJ%!lX1cPtg}85d(ObX_W>|<1GuZI9w>TN{3(i%m`o*%ild>RlQuBh zRz@(V?W}uXE?(@KnS$oqvj+RUglD~o4b5_wn@NgZl(BA+vJ# z{E}X%aGu319~dTGjUdzIz|3i{04QrfRjpme_tB`vxtUn3{`#j|XQ5}Yc}?p$W_49* z&7uLjp8RHj{nMUU5sq$xBJSV8WIZc&7ty(Awg~N6D0MkLna-Vv{KVHFYqHg7rhC$g zqLAId)}!GPiR8!H=R~Fx;&~$GxRkv$K7CFTgtRlm_S-Smp9U?(I|2{ z-7vRNq?WPdeqr)Y3mVbVFN%Ux0JnH$K^DU<3g>a7?GXPeedsYv-`!yF@L10Z7`5&m zu6+V-6%Vmf^9l)%XXymJ5LYo~H{8f<;4lyTs=%WZ4Tw;VP53x3HU5YpQ5#AEnQ>s1 zC+52*N}I(kfm4NGIk!)WYbgz5yIZQ|Q64v1?z25#*$wf^+GHy9^8<@nr9T6%;y}Zk zjTteNV2u?U6WrJ9jZbCQqv}so&eR|`;#94~XxI1OQDmKN_zzxYuu~b@73-JOE3M{o zu2K>heo%o4RWdS$lG-zJP^Nn#Z24vLb%)ZHXUKXm6B^u~(YuG}%a|EcU8u9)26Xuw zsz@I?=EGxH=tTUPM7V#ETzTk}IIxKKsKy)(ZEg3hF7Nf2n{8)_84{a$EURPJ#U?Ng zvTRy_fkNu3pmV|oO(DrjqajWA!YIBCbVDVz=OL;#bbeUty|tP@YUCCyeoWO!Q#Xq> z$(1It9)A9NfLJV%q%3-d0Ya3@uW2fcipa>6XUQiQDCki1(1e^U-!mEN5NLR-pIrsw z8@D5m^RPs zp>lrQRpMSgD-r-9&axf*uRU948xzGyPVyoINh-p^o#~IeoaM@{2Q3DOS z!myj|8($|vQLBmbmS8LFvhfp?$6cdbf(zv0e=T_38{0$6)#;%Oc+A2Ne?u{@N%L?$ z3-?d(B4wd)rC7b9iFS%{-%eVW4K6C3u2L?)_rRD5)yIgPOLPkt8XLPwsW&KSx}c2VEgpCudsJ(*3g1! z%2g_DAYf-g>@yUj<&=t8W`bx_A%PhJO)iHSI31nJZ}>TafkpW6Vi_m-H@GcaIcakP-Xlp~v8(d%D3E&l|Q%TD?r&ST8i!#%$0WkZrv0$q+u zhBBp8Gp(j@Ag!AO};M&67=;e~IBj1dMQ_X~>eU zT2a>E6hgRnpMzN@H!^l&0sB*V8{S=0C8xx+#8f=pr<1WG$!~I?y)Y9^Ha53q#-QKH z(P9$@3!@MhqRlwCX591@pUFh_I6-hzJCP_LTEc5|@?;wK$wR(Kn#EQY?yWaLiMcq-$)}k8Qc>*{+3M*6~Q!y0K=bB<9EXrr-iVWHT##n zoVE?x`EuJXomO=^$Exo+D=MMKHG-X}vfs@QhCQR)Sg<6(I}Jua zG-_1}Ko=TcDP#>Hh^fc_O3L^4Z<8DKS2%Rbud+z!FFjx1kVLZ<8@Og*&4Vnn2LDZTKH33Po16+K=S);4$k3jIG)>oO=cNya3P` z0Wh>6>11q7o=lvSUe`B%rMEvi{^V8fUwuaBze<}&VL$CAQ6UkS(`_jo9` zZ7T`HIM6qOqFVOhp10sq#bycU`F&z`W_3#$N@%IPj_gU@2W^c=b{=fg9JiC~6ehl% zXSOBveU2Upa+#5N_zy+OZe`~VPuJ6m#-I2re*1BLwF8rcK+3qCr$YSnmUK^1!?TLZ zfvyAFM^R;)!nN7PdE}&aCHHI+^~0ouJX1*aZ{wnbGy!2{1U+)0+IZv>HbU7306j(* znqfIqSR&Y1n^2rqZ7y8$0tjQy{&SAfT@#fkd6j2v-Y_mZK=r#;TOXzp#W;&uo_3s>OW7p_0XMIkg=>5aB?}zS5&S~^w443=w zA-2KC)895Claf|bYjaci{S;uKB59QI_hIy+qlnAfZ`gFc5F-#jvU_1Y2t$g(D#WzZ zqC-43*i%L*K&uwrm=&SNv&!uSa?r!%s@dJ^NB1a^1FKS%;htVvY1>+8s0Qz)@x8T| z+2{8MBFF9OSYTn5KTA8Up34$Y%2K7de&kYw9~~HLSLe)Vsip6RON-BwF?El~WEKDh z$VTxx`|$^e4Vee-7v*V)B^Nt{d$1T&E3AA2Pm-xyKhv)NZ!*Ne7yBKl@%$Ozvn;BL{Nig@qQP&Ug#$4+H&Bflo4~82Q7_ zK5XhFTVoYcI7cN3`0TR$V}_bK;MN>R>2`gBzPl1Me<>t!9(ELFLOZxI6gBn6lI!J8 zU6rL7G@MFuQjk!0JvSu@^Jb{qY{X`(s~+%alRT`IYl6kNt>2!qUJ#ZJtH=tu?hfE?4P|5hZ<0n)vL|*7PExKaT(%G&2N8Pj zwiPchO#wa?lfYfsy%j9C5Y|J-_AWFEc*XmvQiTL{7fB4r3ZQhhQD>E9E zC}HFWk&>>sPrNfZvpjpazpm=K#{dqUG?Y_Ny7lbt_@$9gcD%7v)Sn@2`8g|tzTtzU zUDu2Z_bIQllFL5L&9dP;;!nny>sm@0e)}EofASe|MeBRP+u$;EpA~6 zax;ZtgX2agd>Vimy=B2@EoqnSiGxE0#q$p9$6xg2@L)AlUzPj*g|!jkbi) zM&m2n6ff@9PoE8(N48zn%9+u(HI-K(*IPEGs#p|L=i!dxQxiNlJUE*YxPQTcg|!+r z&O2&1_mC!fA zuB4WN%>{hLh+#cN+{2ofxC4NL!N-_^e{Hm1NwRqO0?zh&ajOQ3opKc<^bZoYuw39N z*aqek6OycYl8)jKm%WGc9@fV|vkt_$2&muGIYfA{zGbOXt} zRGk*&QcFDUOtutM*LXu+fzBXzgELw>V7GKN3VqJ(dCc57tzS-e2m01GPZ&)_TS{y~ zF-Qh{?$}$On{zH3i#tkTBhkS6wz{kU87_7JD*>h#BD7BHzjk;5S=m+tNoG>7@6}~6 zjLgjWi`>7X-2NrBD~(*@CCpX>oi0p>g`KQFrKj^f zg}u!EH0u~i%sY4fonykiy#)KKWF&7Wb)r2uUKo-Avz>&dh~^iWWpc|61gi5s>r0f| zgFTVpiyJ3Rtb5aZA1I6PWy{x*+A6*Cah0ZR2PCT%6ss*^yIzxzBmp3va2CR%S^qNh zdfz!&QhFYc(I*?n8}8eg3$WJeDesw*=ReCiD>uS#>27Hsfyx(0wQVR9fI%}nYHg+t zNpDhA1DN!Gcc$282X24KFCh1)+;Bi*$5b~_fPvKZm(Cd3E7GfQYtd4tI_RBn4JjK7 zPm-oYmJFjw_Wl9zDofDfsE5FtjkuZn3s;eu7Q=|dHwoaN0)Dmngfe|o)aD82yJC)n zTpPB4ye%AVe0x#6ZCl-2iCo>EKK4XR7C5-~GOO{%2$`qmY9xt`gj5D32p6{Si zk|tmN;_8;kajmMsjBv6p@G~xy;|Gxn(U(`|ub#FE(`ie$EfY->o@PR=|5V%*p%P-+ zWK4Y~&MLM(hDp9*Zac?u@)DvspvlDw>0y~N-*f+Czh%cXyJ90s0;t$jZta=qZwxU) z-7v}|;P>HEkK`FFscIg*RSy`-qR{*(kUI9g^vv000lSwAblh$B`}x%fmytpWjhJII z!A^e{45${+jNa1aLzVl4qkL*B#$zDr$`_lUz=XaJ3$15o#ph*l|Ll;v7|MNYwU>lo zhJ& z1V^!mP&ok$-JlxV11vLNU1_DQxp&<`zeA%|$@mrVZc*ZBGF4<~N8(HT^Mnpk6nm^2ZsnF`zpAM`<^LfuaW)ltD zOTVzM(#+9!tI7{^sZw;@HGB?^VHvYiU{p8vxBWSOTti+8#Ik2^3eM?c_RUEVSmGc! zlL{Yg=;s65Q{RVoU6v+lW!8Jc2Xq-(Cca`_*VgFrF|HoAI9eC;o(zXg!l@|WO( zKLuf=jTR~1wsdTQE@`_1H${>&lpR@?Yy`iA3geUsdZnZ$my?41;e|)?9|spE#$ZOPk% zRhX0s&QxB+W3vM(DNefK?FoRUYtlvCX%R#aIZ!D@<=7gc1?(_cYM#roINd?BDZiBb z3JjtjLSoD~8ahl)F`zjG;P-PaEK&ooH|I!2!}>x&4|TLAZi5r;HfOC`CQai_VhzM+ zd=tN6`IjWYW5@xq7v9S&6M4*us99gX+hne?l7j8U*SLse-|9QaD>NJMz?XV-${37wP{sF zfZRw6`K3I9;!3&D1i$$^C2_`}LFsc5#((C)Gu%*Y!-H1q^y+Cc=(`lOnT7I4`^5RV z215-%yk(=(F`KxEzs0&B4_^OGehrM+Lt1IL&WeE`_j-9+I&dFcfil!E)?XOwN3?^@ z)}&2XHVm>I=^o`&5g%BW^^7P)1aV&*7~{v-R(B>_%SLaL+Lt)t9QkDs>yMdsT+Vq9 z!-Tz)&?)xocyI!!fGXpl3!mVbx#QcRX%QK8U~qkH0~;CpxT7U&4efz`ksUoo8ja|U zsu4wExb5LUvJNk)ulyjuW<~WgB>Mkn0W3#&E-f30{3NhGI_%2jhgU^%JTf!+MEPA* zN(w8QNx8v!#-rzU=D8vwB|O$)-DZ~;UR1CU{U+WxhR-K9H_1tgx2MmGQiG{6KdBMg z=V=V`)lC)IS9&`iiQDII-fHTBg;a+z`l=yR)#s_+xF^M#YW56Xfgr!cZ#2Dv`%{h);}dtd`Bksp50bia z=EG?i^7)Y6BGajjwe~2M4XUC1@@H0<^sT(@K0t@Lfp)CTm#6>{jtBF6q@41gheQ0P+UG(xEpXW0`mXNmo(}3RgwgMKGMmF= zw~W*>dEv2T4B+`WOx3Jo1eY?=Y;gU$_v4~p9!srz>3i62PaY330C#Gs&i?gi=vrY} zjEn#w?CVdMRTY_j;66T=joo0v8le_HqwLTI5(~j5vjg{`V9GRoZ^#lTyV}|>Sl7c; z1lN|Y8zqU_7MHD{9WfMO?4dLoLS@FA))C?Rr+q=9MwS@T)#JM+a`xJ6NgZCehR~4p z+fjs!mY#Y@>~dJu6xGkOUw=#4$82Q##Oq_QXcZvEs>orarkF%W(l~fEzKJdVSyu^e z4cvPik0h`7b~eCWt?&21WLvo-1 zP_m>w4{z~C!Q+Pl`+AGGuEBle9R4dn;s?qzv?+t@{F8 zm!<3rdG{kIm?a!D*-03{Aoz~E5{o%~2N78%HjnRO_^_VQC8*0E>O_T=jO&fr7w@rv~(~QBN?s6u?;&Jgkd;1DR1EQ2!aRk0NObHcdo8zcbRAHQ% zUsMob-Y?BjkIL;x4LS8`O}UfZ!*W7VqC@6E#*h7;E}KwL>5SLa7uA-ShbdfUb`qpB z3`j3)$TeuJ{TqOhs@!^U|5@oxj0|Dq99OQdFuVwSopqmUTKtRT-5)Bla>vsrrnn7_ z?5al6>-p2KtdmN^JES>X?>v$~W3YDBYtAGMtUXc_k4IQ+)5Jo@`=+s=!H3=$%o4o(ozgik9ejaXY$ zepwXcOhkro^^GMp!6;`i9)uMXl-(oeTc{roV>%^|z3IVSW<(YY&yxUVS+I*Jkg;1J zfFX|sMS|kl2hF}4k^j7#(H?ZUV3%34C-RNVYFb!Hw^saE1e;?BLUi3O1K0qp7*8%f z%tqkr2bXNATXP%^(Pik0t)~r49z9J)KjOaKRx{V`SeMJyR+PaU)ZqBK(}^FXT}V5- zW1%Y30A*;F2&O7k|C+XNm(^ayXh~Hxl#M)g0NVQE@cY*G?B@cUPRo$gY9KaFGthT8 z4j`(s&W6$nf)QTY65HcOHF|)CMmb^1o*NP!C1v;t;wrm%qBSivz3J>S8*MwyRGH6+ zV|U}q97LOezweJ0XpbuiA+@Jlj~qH9>i;5n>!{-jU;^M4OtOrJC<9ZgWIT~F73sC* zUujLbeh{64cV_W539P$(mpDK^_+SI)k|9#Xb1Ro-Ta%vgx>T@kACXQ;HlF zdmsV2!qoS*K+82uyoQ%)K!5%6UfjE-9*^EM6IaZxKuZI#dvgShZDKuODpaNs6~xR$Y+bylnq&6^)UjHyH= z6{iRZq_(ugOCGqhSb;_|w4$jeQy>+al`D?`mRWsATEEkiz{Jc|CPxw7!5WyhmAQR| zq0vnvpb8gM3z&5$-I5N}Vc2STl4#^ZP*FwV*H%l=MD>jzDHT zwvf;K)$0dP*X+Sxzj!_-IL*}T13SCA@sWo&BiH4`^PgCSNriS%{=iS|w=B^7ubBJ( zIwoR7l8o^EFYUpfm?>%F->yKVI~(iPMNr!igOx!(R%+K}vY(B_-CsXO^6yG$P%RMc}M5ATforbJMhtW<5qoGjLj zurl=~^M{igB~lu&k^8s|e2$FVG3cIY1cnj;CV`HMpdi9OHAyP?U#^#-PJLXP@jw~( zC5xoz({21AFdCQGvmn`8P#AxY_^^&xo>j>=ZMPcn@{T5a;SXDJ|1C3d_3ViVFq_f; zZK=aQKE4nCbH`#_yQGYMq)bD`;%DNbbh=Frk@^UK*;t43%Cc|~)2Z%15LZHiR4bMm z2)Z4QfD)Qy6LAy71$WU*hzr;F)l$n-tD8nZBQTZ_h>EkhtZQ80RGd}6LPDAbDYr`^ z!LK&62nu?A7)#GZe`8Tk!8p6+sc4M;kkP+u1O@>CS(9a9&Y{oT?glSD|A%e(@pV&i z^U^7(-ETlS3#nZ(zl5RsCYb13EsE`^_>g97Xs6MKOeUqqt`HvH(}tTDl_1Mx#=*K6 z19`;^cN!-fhr5c96{UbSq1Zsn3x&1zgQ}K5O7k|}z*D4QP@e)NcM(Q4CkFM*`eYgb zjX-}02uVq>UW`mYl?dQydgT6&lu66NjItX~BB``>8(#>>y*~q%q+gJZ(u98ZJoh_j zLE+qEJ}$2)jJpKQN};kht2=Nr;}7mwP=GrxoJt`<3adr1X=enJa$WesicHpj55O1A|SjBie6a?Zhu{`=Jgx*hg`}>=JlbQMtV>-gI%MF@cz z)5Lc-#ySaa(Dh4e&(7va5Z_jLNfSbodC4}hZk^6^o;r*Es}ax$oGtEu4@V^_H(cujtKg&g{ zf-wGlui$Fo=5-|1s;v1eu8bocQT+6cL%4lb0j3qXQN1mu{O+YNo3J146TW)g3xD5mz5Nt&@l^rOg19jO0m1X4=M-e^LQG+}N5 zuzWIb^$4N~T2RnW?vNrNspq8P^6y^Wk3UtnV9OU*qr&FGpI;0z6(`_wO1e4bv}Rh) zAhYYVl6!h$OCzqDnun4c2gEuoH4aYIRDdmikjzFjR^XX;>+zN6_Tuw%^YG0J%aQAF zAm8agzzeJ+!z+Q&XPQU&RM#Kt+f2M~UCa&@Bt5lc74kmzOuN|+Q61~x3}Ad2AwhQi z;h%snKMCBr0Qkce5`r=YLh`YGi2?(SU-)x1P|STE_!2OIMU}O%FaUt{E*gO|LO@)P z5?t3rP$1t=%YZ>K`~y50vmS|ZN)qWyRc4VIPEDZ(cs{+`xIy4lG~t7!3BOO%1Unz6 zm+I&d!Ds5(JwV|ffd{`l{4{~x(oG{UGzch{BQo*T_1*Yix&Zy^`Wcv>@50)5LI}|H zLW+Qd!%6sp39MdZMbTsve(~a7JX!0*U6;*77Hb^Dxgejo2n&lU8X>V%NIv7eO+I}2 zp^f;jSN7rYTV~)tZd!zbL?I4W+wjI4F|2(nf^Jq_)Uafm0xGB29SmUBGiMKo4w16TU^7aKUt7Q<^5cOq%e4mB-TrQ6LRRli8u0 zMqo$~Pzy5uytAhT#V!jjWI%C!T^wx;D$b%ML24ru<{9yxOPwgLOyYl@*^dAB+7aA; z+hWX^;Nn8gw5TUo6GZrs^Ey~bxSQ3M?|pGUZus>(IIq-(jbB)W)e|RT?bKn zcpnMH1Hjj*W%}VCfq(rl_m_uW-H0LYw!VNyVC*9x^NE(|82wN~F;RMOiXCTB=m!G2 zEQv)y=A|Uw(80|8;H7_72pmTfo;*eqKA5HnA0bWfk|s2fCVY@IVN=V=G~q|23I9Tx zApFd^*43GnuN{XXw5TUzrthyt;A|1d@Hm$*n26spe&A^qKD%{gC9W_t_JHo8lPV2( zYFiy{`{N$`_jR*z8TYeObQK{&ng|D(=h8xdd6q#yO-m5Je3|Y^42pi>%1T_ja1uN% zMm+ao2p+l@IcPnQNu=F2&i3x{cp;*E^Z8Tv!$7M7^>ExQcSX1U5ctJkfbU)hRFBUU-VfwuQMlmy>~Hb=4fIi^Y}V7SwZCiX z-!%dnfxZzCD|MK`Z(ZW^$Il4~WR?`EeKuJGMV-s_k1-0eFuEuxSZGmDJd_X}-82II zARsj1zpgz-6F$0yd{AzhCOpd|7mQW=Mw%vkR%pUaqzMcS{1R!x9MXip9VVJ!&Y}y4 z1e3<(#pgfc+M$Jpe(t#bxnBfiL6%_TMf9!s_xH}hT?{mS{Xhq&}HtD~|BOuS7xD0t{*!uH6nwD=l z_WRoh>2rD^Fp(zb3+FLrp#xY@mHIu-V`>_JrSwsqH3_(p+c!{Y7tAN!=sKPz93)N1n?}VEX~GSa!$%X$Pj0Bk#QZFjoMv=A>M1DnaS=5UIXjg=K^a3#+??z(Uv<+6^oD>uAM)r3 zEvTa>MF-b?BR2*}52AcEktVc}ChR>-n!tU8CIm?nY@`YM4>22_J4I6zO{h=Ngx;<- zRNKw<^=(+cs{wm_F_gG%c+cEYEM}BNaSnZwc~!B_#cBK20>e<2vypUopCaSW15EY6=hEeH_SoHYFrwLIT14pQonjCzWFd4?Pl%z(_$rc$tK_4^dd4M=KJ{T%=@xemls$NYqj>3Q4-c zlK7}2IoWm72=s%12nS#a_}*wj3*!?yXmvP=CcI2V5+Aka4v;2P^>dLS`q}*PYv;jk zU`}Cw2pgE4=8v0d@t{&$ZFDK>q%`WMDfqijZpDuF0B)a_gYVw84EdHU zG&Zm>4r?s5b|kQ~J*?_2$oeex7ewQ&g@nO>P(&jx^W8;t+tNj>fX7V#zPhz3Yl7CF z4G3^iQo1MvKk`-Pz|K!u6yEbM6b3#5oIjh}Ujtrv2l(Yzs9B8gJN{{2Sa(k0|Y zc!j_JIsZ^#&`l$t5g2>~l5q+Oq&Wt9P^5~j4e%P`r>$O01V=(7LWlNs9Uyk&eK%SN z%v8E%XaZSoTuYkposTdrs?dbBz>A~_KmUhgG~qwq4_vZ1MH9aKGXgkFG{OAKJwD`8 zT59y3{IyFh0ZM0v5N9~W-yWz zs&WolkS(mqAZbyhX*Z*R;eWb&3EdNHjZ}YJcA*_PG<&x1_#@qp4yLmnk-+NKD*}R_ zw2}g#l5Sf~ZS+5-ppZ>@%S^AW1cd@=1;jOrzms-=Av!7@YCerM(j-Uw>ovg5=cm5Q zdaIj8KqD|%2pH(@$B9TS6ImL>XqHyfRwi0{{+{|u{E?|qm9r7|wOMEd=Y&s+wkk96 zPko@?I*t*@JW{2k3A0E8nwaHIXo8zG!P0{!u*8%~n|&&pAdlGEgN$dmAO&RRoaYKw zvoQZYGI6goh4Ct1m02b%Ds6{`nVY6JARIfE zk>*@=R$ph7A;9N^|8b<9_BQ%c6p>pB(w#-pqgu)I`)Dax`*svL6dH1~on%gpXpbbY zv4aKKScuI|OO}g8*~EoOn!IN|r?%+7gN=aT8zZNB#f4TB7aGvH=V=5RUWd`fl})=G z^$T}K&=UXB+m-?MJx{C5jMQPPCj*bIqpKaQYR|q6>^uTI`~tN{#N5i)q~g?mZ|$JJ zs*m}!XQsXjGozbEKqGMW2#A_f=BQt)GIOzGO=fCEBz|sSElHOpqp~P4!=A;g6sGjf z?pwYX*s1kiV-$f5O|X(Ce26sRUZDx;wSCnT;BnFf33UF;Ta3+Q48rgJ%IA~%II-nd z6s16Ub32n=kd4aFgmW#Inn|Bv=2dA>P)t}oooV+S3?Q3~+yx~zeDLyFxOz!B?tl3J zZu-pzd~#74ZdqN4iUJ#AbWey<)Dfv#)3VeJ)qhh62*YWn1E`k_|KnR4aL2S&BDOy_&Qdpgfzq0s9rR+7!&8V1YLU-+( z@Yg=8g@n@uKfM@!dCScc;>2fAt~8Cy+J> zO-Nb%&n;Gp1c|U9=5ArtY*b$rQ2Q&a|FaEY+_R$_Z3Y{@e)9sn{_%xqXF1U;?|lmo zuRR1$IIi;6%Skfz7|w0?_0>iy0;0K*Sc0`X8nE$*ht{Y{az+N!urjk%{6g78m{0-* zpE*10AK3Za*=R)&hE;?F-Oe2XqO}$wp>mQDvu7~#RNEeO?|XzyZ60rutPHvh6am3f z7V}a<_uS{0rQ&vATP<+t`#=@V&tJcV0>KTy>M1O;b{lIYFc#$5uLE~n2CTT2TBCB{ z_RCXOlXK~&5zq(>9s)9@B5BtnrQuYSnfp1vqDNxgrbR*Q7!`E-=0hpzCoJs#rKgWOeRhE5VPK8rD(#3Ne2{7s5nLw`Yrh}1bCmMtCIO3 zLW2;fJX#e*!g-iBk6%~&;4H|+Pd~mKkKZ^Q&u*>5jrYBU7q&Jr)KStX@&HaAIbOH% zjesniA`CQoBKQhZpMGUc8J12iz>aDL#nR70ED0hYWSV@G@0nL-yM8}+2*^E zsM?^4qx-<~6xev9L~7Hs`m}F^U@6b@RWvpK{ueYi?*eKYfUnWY@U{EsT0{#2z3uSu zOSHUEpq@M-_4&h}0lxp~)a682R`8$p)%5`y0gb>Z5s-zBOIloVz;l8cgzJ)ZZlFyS zTT7k9#Pq1LI_jno7zhGl89pUVxQ5E33{9YbAT&WT?+Q(L=tbbp&z?XN26C}M-iO^e zb(5(e0)r@j^BL@1A4=dC6c!fensMoj33y^gG5)fljzzR~W6isD`0ORquz+Pp#f@1N zZPOI))WD9Ht$c2t*-#5RvrByB!YX*%lGt}BrUXiDEf{$ZfoMDI46Mq@F(4l7KrGNe z`bjTInb5kOF#NX~~wi9q$n*PU1LwkPs4*Kv-dv(bASt%IKgitF%B% znH@k{Ac3D9Ac2JJIKxgn;%!-8vSnF&?{ELlbFQu}8`=J3$+pHh(zot7_nhaf_dV}I z&V;u~`kV+9lD#yi+jtF*lbm8iv0B)nL zLgkVvcxc@r-2a2k_`4hD;LTUfAC6H zuptC_qzkaeSw>-O^#_ept$&J!O|@IZ=z&V!J&fX{T?x;*u?()fTNC`13xTavJ$iK) zbx*k{)HXU0%Z2LV={&!f336AH)3ns8P|!EHRQ zb*v$MgHyzyh3wge5H_`h@Vc@TeC)18m|vEKcm8BIB1GW+wX?y{42Hx=WbC=mu%F`} z0d@^zd|5|NN9`R!ESsKVBGR_B#prk1n1PM|N>0QYu;H{pCP{oW8Y1Hm=8hS+ytVyd z(>Wuc`A?W?VUXxqlfPa=C0!%t!rCPag^oRD5f~i=r18)l44Q!!6L%mRg+%AM7&lNf zC32uDj)I*XN2g6DQ!e9iO-Ptga2wLy4bGwcLWB$3kE;3@O)iSpUp5W@6O7=45AVa= zk}Ry5UufK5nw8JJ*?vxV1k51d+kv_cqeC%gN)~#$88nUY5d4%hBsw2Yc%a5u1=TKM z6hx$k8HmVHIZ|udG#UuFHVc|R4XQ4*E~UZWR*EnhNZULt0;fknjCh>l(_VQ!(s#Zk z8P%iH-*e&&2P-A5X2>$5fREx=%PO3J$9@|Y0^?~-IA;O3Yw%o5Pc=_9hEdTR!X0E5 zZYR!dQHOWV%f$>v5vi%AOH|Vz#$~l8 zMWNMplK6;zjf+vRBX`u>T||nc3R)nPSUJtlSN?o2#l9C;hY%-LE;WA3Ky`aEleM;UR*If8(W)t z(A6JtcF61&^6-1lIK9KTMCGpZp@vRusP4dBE2h93Ny3q1QLA<|hTBxjoZEKhyER}Q z!>whDW)ZLmj0geJB+;-VQ7|5l#4IG3#3&dW1yXpa{(zB?TQ(s<@a$z57l9FU)Wk=~ zX{sn@L7tIXf-z(CJi8C$18Nfiu9rkwr1QwpKU{v^QB2tTQcg`%qL(XhE61eY+Wn@LnGkI&W1jlz)=mF00&$h z8HHB5P+c~YQHCgD*m9r=3raH`ecS}{v6qa01f)t4rd)PUZ3k{HPD5cvGHMS-t(T(< z*tCZD=kr3;9*W3_XvhUHiS`IdhLLa$XysPiD~_Kj7V74^yE2+X_RS(-5wHl1dj#b5 zC`>)rF6qK@ij(S*jwPN>Y<-^imiXQF%87x%kjv`CsM?UV7%`)uTuF1Fi-bmHTL@Q{ zWZ>O%a`4!m2Hbi145a%#M$#~xp*Uno?ZGjPfa?~ulZNdZj&1iISh0$GSeFWJV&UF#S zQLxET$C7G7EKI>Ri)XRV>=WuhaoNt{gg6%WTVK5x7VQ9H;V6nHx z>N$D%;qMQjxw8*hj71%ZC<5ZS7xF}L?YFUtfQF3>HJjMx)l@xONO!q%#oyi$<8j;e z=eylBytGM{pPPimm-sPlsuzBABGS_^ph~1RaApqQv>e2|x%4OQArluij9e(10)GlU zne{dy*s=pjK8A=?aD~a7SpOt%0Fmxvgd#%kD5El6o*zUt0M!TjI~Xvp$&`E2x8M0e z+U7dW5zwYqWTZqs$D4+nO_K`&bsNz$t?jgP;d*Og?(x`ICU1LsV(97_nfmwWtbAOc zq=J-5l_PyNbo8SrD;Zg-0rT7C!6#RjntZj@U^&`EwG|Rrb7-s5$GBbHy>X-FGSYc@ z@}W7r8VmanLp+w2cra^*7ky2;(X{)=G}`0tGIyEHOvbS(V$$QwfG;f{UVjQABrLTv z7RV}tH>C*ut^25Y)JZ}!l^0KPMuDV2k|zb>?rMZOs*#*Ki%^ky7>4oV@LKWQuXf6% zcCRoJ5Kn+Apw=k;+yS+BPDlhe{c%)_QE;oLBhFFx$XP#iIg=O;n7&9ZXxzwMY8KUu^u4~lAEl{2q$D#S ziN?~vz?xiLYI4=q?F0L0pD-sofU?{)v^U3$u6(ME+jags1IL+%I~}0`k!Yx^fji+< z@Mq2A806N+;KbjNo<@d_C(%2wOWxbCgDW3{tohO`lQ0-7b!=kWb@*AsC>SQeoq-2m zcM-oECV{@-qoFSJ)I87jv(li8*c|rJV;KQbP8z<7uPl{mLz^6@PmX^%eGLXtgbLhd z(k0^GzONq1#dBTpJG(k|(t@ub8uIjMfM zGmt{exCh`#JDx@k@)*v+v_IRuj~mz9j&gC9q_YQ6<_jCNQ9R|O6tyF3}{#3&#h*|IiGT=$h=kEZ|p5wN|_<(A7 zr4%#ePe*^tc63%f1#G^%acRgV-Hyb zECL!gLB6~va|ve#W?(q!!_|lbDT9$A&fSSkliY=VsXh68MU{<(d(pb{ zN9b$Z%=XS?S=a$=vItlNhD1QBM`4av5kHE|oKPC#GOK!&5ONdpBJF{vB-6X+jxe_F zbeIJZ=NENJAkuoiYi#}8%OM;(5;f&?_w3KLF-Ar8NNc1d3dLz&^!5|Dre55$Jdb{c zWo>N@na{Ox#>;(devn>4eH*v3dqcRSGz($6saE_1x#3j*u<86EpvfUjrQ7+le3(7c zN427QG;jSK_395A0@nBmw+z4t8-S7p8hCdedIZ6?{m3Z44#_z)NSLRcSRF?AgyG#z zW4d4``kO1!b>tZuq1Rg`VR!)At0yS}W zh|!6N7sa$$&@q=`xRA9Lnbk*|(4%!x?5v2QgN|gx%#4&tP#s2vQ4lBst*t+b=>>jV zMb(q_Rjqg%<0_>2=pL1ul@|lJ3)x7M`B`R~<=gJ+Hd^|4P)<)*-L$fo0nJEt<0dn% z!>PMe{&wsMW7;&@1%-Oe?y@&IZ(udl6k^lJ5#TPQKO3R;{WNB)fG@R#M&ZTAu4CkR zpCt!Pux1j~WXqsWVS2P8a}e#9s!^I{63zmjJ#7&I4A!uhW?A8nF)u*xBNtG!dI$8j25Cyg4D zpofeKqo6{{J?hrhFjkeM;La7Lc*jrn;7C&!mQ2k?ge;A#*Jo;G-zE$Knk2;U| z8RMqVrD_Uw{nYujQ?BdJ)Q$Sc$w&FH09Buq@2cfq%$$~D?m1c;F$PDU%k?Y z@~J5%n5fF zZQLpbjp|>sZ$W0+3aTM7s&KRy5$dytd)v|9egvWJIuk2FM$XS?qvp0x;{=)&hnW{%OESq8opEFW-BY(8b;UXfS3D-xpqfL7n zv7@E~U%F!@;=v>wsiogu9zPwEpur@<_olT0WBlV*VaxU~HgAuZpkX(zO=f@)=gi@w zQM|B$5roN=x&u*XL|nTJ#QZrvWKyDav@UF<5z7|(QBhe*Mm2(Eix=ToT?|KRqDCyI za=G~^kHPDNe!J(~a+%$F8s(Q1dT6lZ!_M7%Se7I#S+D>#G#qMZj2VfAp{JVC`fFBV(R@FidpU>znI=hDOnC?Lckfp@m7gRJ zl1M!BVh}|I9^@AkB0as3jDy2?h zyqKHo$1h&0qMbqzpLz2vcsUT|tJq2tCT17XBNJG?lmGBdR2*u+{>{H2 zvCL?kjPudk$BCZhmS+N`AQ=FuKY2XP;t`7{nH_oEuj`e>6!g(hKVIP6^X@sx|Rk18HSnmkOk;gDa&NG-lje@AzXP#*aIq+;(^x42*ceVAsERWbZ>MnAy zO8qJ?*Ngo8Y)W!+%x#jy)YkT4`cw~AEM0`0h3`b$?jM-w&2GN-(;{FIun0^D1hjUj zBATd`G&v6^4Z2(#PBvKtCIbTJ%qVD#kbXiRcR3eilSrdJ_#@jJaNF!W%qYqL!zoxM zVFLFcwZE6W=jCm6sI2S8ckf*Q52XplHi6QFMCZ_TFK)umAN)FgzODi-TYrpeugZs1 zg~H?jzW7fMAnzL=GK_+Ug!E<01BMY0fgPgi{eeSKN(q8w68v~}-Fp1^7f<1Tzx^3p zd!-Xfr>KBF<*B1DX7$>ul5yo_$%fh3zB|bKxSP)YD*}1Rf@Bi@&f3w~1iZGhABFiI z?&hbMm;~(|Q4|(nsN;67xkeTWHDCExl+*#?Af)Ec9?oykotB=z?SrBt)`cY0V zM7>m<5<^pv@7&+s0jLZ|kHwHohH53tf5}`Qzf;n}Jdzb7UWw(_Gw-}LgX&H!EdM1j zXlMi~_6Hfz?vGWH-1Se!RY?QPkjaY;qJ^!wMG_FIh7B~3)=3|^h^E#UQd7dXWZp`8 z)agS<~d76FUE7(u}7=5PY!+&BC*a1(F05{2QTXYUxF2%Iyc zpa5Il+%27#!JWwBWW2Dm4v!!1#P{xBX*?jwZox_u#%HHETd9pSfgPZ!D}t}SP=ya& zR?Hx4d8pVKMh|zWWp$sZJwR3l{m`ec0gmj)*S_^2zVJ^UqkRI6;z?OgT{0bM1Nuj| zUY}}+`K{Y`;NYRdm_B_PR;^fy3AFaa^Prrn`yi8oSa+#3`^o1g* zZ*0T%?b|6mAXBk?5pKF>DqeUcge>;m<%@kJ*pK0fr*@K158#T`EAhtLoZa>#M~{$U ziDAynnb`2^CKCMpxaOKQW`PJYU!_&c{B)Jsf=yd0(bCdFW*{G{E?tV*GcwF()sEe} zQ97lBM7R&nJhze3362+*;nwR*@H|zD=FAFU>XanBwrvOYA2@{7mtRH<$ptYf21#D7 zjIx{Y;>)|y)6;{Zq9QC^JdX@cHh%wf5bYg*%q-|$cP-TS!AVjDsWF|J{FHL6-%La3 zEH6sS*Dwh8vEw=SVR{(JGMzj_~Tf8F)C^77?0V2+_D7)CJY z5ad1|``8D*fFC{ZU0i;{>#_0K$MKeTy&Dfc@D=>;L%+svfBtjKS-J|Zd;jO~_M0!m z|NZA;s@J6A+28NMb$9+5j<$56_rPI3u?MegdKA}Ou}qK0|NHqPc*k4r#q5=Lk^!N+ zCT{m{+4>Mxl9>PR2Oh=`e*GfuzHue~=j;E7MQh%UjZd#;-5DFicvO<=QW&?t@;A8j+B@*nZ~hCpIa&C?N4`W)U`gnr{m%X->P}Tv@~K(avFqo!Zp}RA z6UETkNkLli3Iv;84c4;X=>z!ttEXZSRdM$24^xj{u1JSh3lZE|8Oiwb58RLCSKWgT zfAUMl06Cdqz1xCO62gw^%g=uK7@34G^Et2K)n^`}EyGs)?7{!Q?;d*^pZe&ZVC}89 z(a?A%dMh8pkH7cN)c==jUQOv6@rA#AFM9Srj{OIp8N{W?3Fyd;TZ*_P>4v@k5Ve+s?;%{3w3%^GD73 zO|R`TOu~PB>;Dit_7rwJ_kUP0i+W6`f}f1SlTW|E@2{X@#*)s4cOX&HsF(Vn&x=O2M# zl6BlkCh^clCgCy~MXo7J#Urm)jv!HRY_yt=keOsT|7Dqk3S_s8Ys-oGOY$ z8Pm(r)SNazJD;GSB{?fCDaP(S`|-QSpTov2J9vke%mt+fOl{2PmOI~tTW(lGMhf)6 zG#^*qa4YI%Xu!06$8Nr7;J$lrr}QKVB}E0e|7~yJF&3D3uUxTMm*F4&=_`2QJE^PG%p09G)WD&3k zB#waDS#dZ8vzaukA?#jE#iQfQ#_=zSm%&~+;Si8;O6#%PLoRd$k3auB6ku4YM>77D zBw=Bu7x&J|z>B*Z@V;MF;&XQ`z@@YD&`+Ied5E!T;vt|((aWAFMWF9IS%FPW-FWT8 ztC15+#^YN^0W+zio|rt%p|ieeS0dUX~*Eh>+ih@m#$c1o}oa0J_ZX@JcjCHVcG>GQ>`b0MYD=|K8b{N zlqx)4XCMy{SB*_AJXeG#pLrRNJ@vAYgym$W;JttGX_kqS192Bvg@U{+Y-W6d!!=>X zIdFWohGnhKw0-AZEWhn9N!Q2l-uu?l*qGMFMk&vn=9q|J-t1Civr$7M?OD@3l)8A1 ztLC+~w(-KbbYq%~1B^dWQskrIG>wRvdVBkDDOI4Jer^N4_;26AwO3z`g{$wy&;IX= zhT%AHxDWk=UPjcz#oat$;7#qOKyp)xk}z|I58Yi2hN)H@QSt8VOzCyEZP-RBor({9~#Bx6>9@ep)afWYMshEgH zeD%wJ&wFN{nkA(WVibIaf$*}7X(=amZza7@`qEdwLpP;y)K&fl`MKGsZD__r-~H0S zbDXuvFb3LbU|pP-2C!3x!omXPvIpJW{a7$B$CUeJ+EWZpH(qxI^xC@PQ&@ZFU*Hp; z{wn_P>+jMC+K)Z^!boGC-3e*%eTjY-!kS>_&hneM%)|WvSCH&PIGbaqyguPHS1w%EN8GGh5FTE3bjq7*U{klu2kD!|+ zgq@xv)$i7}7^L!yE}u`~`5cA6*I!{ZhK*(IdXjpzFPE%@xC75K(ob8-E=BD}_U1@#OzmXuO7P`6|VpU>JljrGZN}f(6saTrG+{r8!aU?{E+&gKm5tBeEf-hq#a^~NI
  • o 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�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 RdtjiCw79%jm54#${{c=4gx literal 0 HcmV?d00001 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f795b201..9ba90f87e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changes to the Mapbox Navigation SDK for iOS ## Unreleased + - Map can be used without an active navigation, navigation can be started anytime with `startNavigation(route: Route)`. - The `speak` method in `RouteVoiceController` can be used without a given `RouteProgress` or the `RouteProgress` can explicitly ignored so that it will not be added to the voice instruction. - `RouteProgress` is now optional in `willSpeak` method of `VoiceControllerDelegate` if the `RouteProgress` in the `speak` method of the `RouteVoiceController is `nil`. - Uses the `Locale` given in `RouteOptions` to create the corresponding `AVSpeechSynthesisVoice`. diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index 2b6d856bf..b81f1836e 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -34,12 +34,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.viewController = NavigationViewController(dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ())) self.viewController.mapView.tracksUserCourse = false self.viewController.mapView.showsUserLocation = true - self.viewController.mapView.zoomLevel = 12 self.viewController.mapView.centerCoordinate = self.waypoints[0].coordinate self.viewController.delegate = self self.window?.rootViewController = self.viewController self.window?.makeKeyAndVisible() + + self.viewController.mapView.zoomLevel = 5 let positionCameraRandomlyButton = UIButton() positionCameraRandomlyButton.translatesAutoresizingMaskIntoConstraints = false diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 68f5436bc..663146137 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -24,8 +24,8 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { - parameter navigationViewController: The navigation view controller that finished navigation. */ - @objc optional func navigationViewControllerDidFinish(_ navigationViewController: NavigationViewController) - + @objc optional func navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController) + /** Called when the user arrives at the destination waypoint for a route leg. @@ -472,6 +472,7 @@ open class NavigationViewController: UIViewController { self.routeController?.delegate = self self.routeController?.tunnelIntersectionManager.delegate = self self.routeController?.resume() + self.mapViewController.prepareForNavigation() if !(route.routeOptions is NavigationRouteOptions) { print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") @@ -595,7 +596,7 @@ extension NavigationViewController: RouteMapViewControllerDelegate { func mapViewControllerDidFinish(_ mapViewController: RouteMapViewController, byCanceling canceled: Bool) { self.endNavigation() - self.delegate?.navigationViewControllerDidFinish?(self) + self.delegate?.navigationViewControllerDidFinishRouting?(self) } public func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { @@ -666,7 +667,7 @@ extension NavigationViewController: RouteControllerDelegate { if !self.isConnectedToCarPlay, // CarPlayManager shows rating on CarPlay if it's connected routeController.routeProgress.isFinalLeg, advancesToNextLeg { self.mapViewController.transitionToEndNavigation(with: 1) - self.delegate?.navigationViewControllerDidFinish?(self) + self.delegate?.navigationViewControllerDidFinishRouting?(self) } return advancesToNextLeg } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index f122ec821..4de021bcc 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -172,34 +172,12 @@ class RouteMapViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - self.resetETATimer() - - self.navigationView.muteButton.isSelected = NavigationSettings.shared.voiceMuted - self.mapView.compassView.isHidden = true - - self.mapView.tracksUserCourse = self.route != nil - - if let camera = pendingCamera { - self.mapView.camera = camera - } else if let location = self.routeController?.location, location.course > 0 { - self.mapView.updateCourseTracking(location: location, animated: false) - } else if let coordinates = self.routeController?.routeProgress.currentLegProgress.currentStep.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { - let secondCoordinate = coordinates[1] - let course = firstCoordinate.direction(to: secondCoordinate) - let newLocation = CLLocation(coordinate: self.routeController?.location?.coordinate ?? firstCoordinate, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: course, speed: 0, timestamp: Date()) - self.mapView.updateCourseTracking(location: newLocation, animated: false) - } else { - self.mapView.setCamera(self.tiltedCamera, animated: false) - } + self.prepareForNavigation() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.annotatesSpokenInstructions = self.delegate?.mapViewControllerShouldAnnotateSpokenInstructions(self) ?? false - self.showRouteIfNeeded() - self.currentLegIndexMapped = self.routeController?.routeProgress.legIndex ?? 0 - self.currentStepIndexMapped = self.routeController?.routeProgress.currentLegProgress.stepIndex ?? 0 + self.prepareForMap() } override func viewWillDisappear(_ animated: Bool) { @@ -226,11 +204,55 @@ class RouteMapViewController: UIViewController { // MARK: - RouteMapViewController + func prepareForNavigation() { + self.resetETATimer() + + self.navigationView.muteButton.isSelected = NavigationSettings.shared.voiceMuted + self.mapView.compassView.isHidden = true + + self.mapView.tracksUserCourse = self.route != nil + + if let camera = self.pendingCamera { + self.mapView.camera = camera + return + } + + guard let routeController else { return } + + if let location = routeController.location, + location.course > 0 { + self.mapView.updateCourseTracking(location: location, animated: false) + } else if let coordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates, + let firstCoordinate = coordinates.first, + coordinates.count > 1 { + let secondCoordinate = coordinates[1] + let course = firstCoordinate.direction(to: secondCoordinate) + let newLocation = CLLocation(coordinate: routeController.location?.coordinate ?? firstCoordinate, + altitude: 0, + horizontalAccuracy: 0, + verticalAccuracy: 0, + course: course, + speed: 0, + timestamp: Date()) + self.mapView.updateCourseTracking(location: newLocation, animated: false) + } + } + + func prepareForMap() { + self.annotatesSpokenInstructions = self.delegate?.mapViewControllerShouldAnnotateSpokenInstructions(self) ?? false + self.showRouteIfNeeded() + self.currentLegIndexMapped = self.routeController?.routeProgress.legIndex ?? 0 + self.currentStepIndexMapped = self.routeController?.routeProgress.currentLegProgress.stepIndex ?? 0 + } + func notifyDidReroute(route: Route) { + guard let routeController else { + assertionFailure("routeController needs to be set before calling this method") + return + } + self.updateETA() self.currentStepIndexMapped = 0 - guard let routeController else { return } - self.instructionsBannerView.updateDistance(for: routeController.routeProgress.currentLegProgress.currentStepProgress) self.mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) @@ -268,7 +290,10 @@ class RouteMapViewController: UIViewController { } func updateMapOverlays(for routeProgress: RouteProgress) { - guard let routeController else { return } + guard let routeController else { + assertionFailure("routeController needs to be set during navigation") + return + } if routeProgress.currentLegProgress.followOnStep != nil { self.mapView.addArrow(route: routeController.routeProgress.route, @@ -303,8 +328,13 @@ class RouteMapViewController: UIViewController { } func notifyDidChange(routeProgress: RouteProgress, location: CLLocation, secondsRemaining: TimeInterval) { - resetETATimer() - updateETA() + guard let routeController else { + assertionFailure("routeController needs to be set during navigation") + return + } + + self.resetETATimer() + self.updateETA() self.instructionsBannerView.updateDistance(for: routeProgress.currentLegProgress.currentStepProgress) @@ -320,7 +350,7 @@ class RouteMapViewController: UIViewController { self.currentStepIndexMapped = routeProgress.currentLegProgress.stepIndex } - if self.annotatesSpokenInstructions, let routeController { + if self.annotatesSpokenInstructions { self.mapView.showVoiceInstructionsOnMap(route: routeController.routeProgress.route) } } @@ -507,12 +537,18 @@ extension RouteMapViewController: NavigationViewDelegate { // MARK: NavigationMapViewCourseTrackingDelegate func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) { + // Important, this look redundant but it needed. + // In NavigationView.showUI(animated:) we animate using alpha and then set isHidden + // To keep this in sync we also need to adjust alpha here as well. self.navigationView.resumeButton.isHidden = true self.navigationView.resumeButton.alpha = 0 mapView.logoView.isHidden = false } func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) { + // Important, this look redundant but it needed. + // In NavigationView.hideUI(animated:) we animate using alpha and then set isHidden + // To keep this in sync we also need to adjust alpha here as well. self.navigationView.resumeButton.isHidden = false self.navigationView.resumeButton.alpha = 1 self.navigationView.wayNameView.isHidden = true @@ -580,7 +616,10 @@ extension RouteMapViewController: NavigationViewDelegate { extension RouteMapViewController: StepsViewControllerDelegate { func stepsViewController(_ viewController: StepsViewController, didSelect legIndex: Int, stepIndex: Int, cell: StepTableViewCell) { - guard let routeController else { return } + guard let routeController else { + assertionFailure("routeController is needed during navigation") + return + } let legProgress = RouteLegProgress(leg: routeController.routeProgress.route.legs[legIndex], stepIndex: stepIndex) let step = legProgress.currentStep diff --git a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift index 6a342694f..743baf52b 100644 --- a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift @@ -183,7 +183,6 @@ class NavigationViewControllerTests: XCTestCase { // wait for the style to load -- routes won't show without it. wait(for: [styleLoaded], timeout: 5) -// navigationViewController.route = self.initialRoute navigationViewController.startNavigation(with: self.initialRoute, locationManager: SimulatedLocationManager(route: self.initialRoute)) runUntil { @@ -261,6 +260,7 @@ class NavigationViewControllerTestable: NavigationViewController { styleLoaded: XCTestExpectation) { self.styleLoadedExpectation = styleLoaded super.init(dayStyle: dayStyle, directions: Directions(accessToken: "abc", host: ""), voiceController: FakeVoiceController()) + self.startNavigation(with: route) } @objc(initWithRoute:dayStyle:nightStyle:directions:routeController:locationManager:voiceController:) diff --git a/README.md b/README.md index c7cb5fd8b..d13110e46 100644 --- a/README.md +++ b/README.md @@ -42,114 +42,15 @@ Install this package using the [Swift Package Manager](https://www.swift.org/doc - **Have a bug to report?** [Open an issue](https://github.com/maplibre/maplibre-navigation-ios/issues). If possible, include the version of Maplibre Services, a full log, and a project that shows the issue. - **Have a feature request?** [Open an issue](https://github.com/maplibre/maplibre-navigation-ios/issues/new). Tell us what the feature should do and why you want the feature. -## Sample code - -A demo app is currently not available. Please check the Mapbox repository or documentation for examples, especially on the forked version. You can try the provided demo app, which you need to first run `carthage update --platform iOS --use-xcframeworks` for in the root of this project. - -In order to see the map or calculate a route you need your own Maptile and Direction services. - -Use the following code as inspiration: - -``` -import MapLibre -import MapboxDirections -import MapboxCoreNavigation -import MapboxNavigation - -class ViewController: UIViewController { - var navigationView: NavigationMapView? - - // Keep `RouteController` in memory (class scope), - // otherwise location updates won't be triggered - public var mapboxRouteController: RouteController? - - override func viewDidLoad() { - super.viewDidLoad() - - let navigationView = NavigationMapView( - frame: .zero, - // Tile loading can take a while - styleURL: URL(string: "your style URL here"), - config: MNConfig()) - self.navigationView = navigationView - view.addSubview(navigationView) - navigationView.translatesAutoresizingMaskIntoConstraints = false - navigationView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - navigationView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - navigationView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - navigationView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - - let waypoints = [ - CLLocation(latitude: 52.032407, longitude: 5.580310), - CLLocation(latitude: 51.768686, longitude: 4.6827956) - ].map { Waypoint(location: $0) } - - let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: .automobileAvoidingTraffic) - options.shapeFormat = .polyline6 - options.distanceMeasurementSystem = .metric - options.attributeOptions = [] - - print("[\(type(of:self))] Calculating routes with URL: \(Directions.shared.url(forCalculating: options))") - - /// URL is based on the base URL in the Info.plist called `MGLMapboxAPIBaseURL` - /// - Note: Your routing provider could be strict about the user-agent of this app before allowing the call to work - Directions.shared.calculate(options) { (waypoints, routes, error) in - guard let route = routes?.first else { return } - - let simulatedLocationManager = SimulatedLocationManager(route: route) - simulatedLocationManager.speedMultiplier = 20 - - let mapboxRouteController = RouteController( - along: route, - directions: Directions.shared, - locationManager: simulatedLocationManager) - self.mapboxRouteController = mapboxRouteController - mapboxRouteController.delegate = self - mapboxRouteController.resume() - - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassVisualInstructionPoint(notification:)), name: .routeControllerDidPassVisualInstructionPoint, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didPassSpokenInstructionPoint(notification:)), name: .routeControllerDidPassSpokenInstructionPoint, object: nil) - - navigationView.showRoutes([route], legIndex: 0) - } - } -} - -// MARK: - RouteControllerDelegate - -extension ViewController: RouteControllerDelegate { - @objc public func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { - let camera = MLNMapCamera( - lookingAtCenter: locations.first!.coordinate, - acrossDistance: 500, - pitch: 0, - heading: 0 - ) - - navigationView?.setCamera(camera, animated: true) - } - - @objc func didPassVisualInstructionPoint(notification: NSNotification) { - guard let currentVisualInstruction = currentStepProgress(from: notification)?.currentVisualInstruction else { return } - - print(String( - format: "didPassVisualInstructionPoint primary text: %@ and secondary text: %@", - String(describing: currentVisualInstruction.primaryInstruction.text), - String(describing: currentVisualInstruction.secondaryInstruction?.text))) - } - - @objc func didPassSpokenInstructionPoint(notification: NSNotification) { - guard let currentSpokenInstruction = currentStepProgress(from: notification)?.currentSpokenInstruction else { return } - - print("didPassSpokenInstructionPoint text: \(currentSpokenInstruction.text)") - } - - private func currentStepProgress(from notification: NSNotification) -> RouteStepProgress? { - let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress - return routeProgress?.currentLegProgress.currentStepProgress - } -} -``` +## Sample code + +We do provide a limited example app but its not functional right out of the box. + +1. Open Secrets.xcconfig and add a mapbox api token. This is needed to obtain a route. A free mapbox account is enough for evaluation. +2. You need a maptile source. The example uses a demo source which only displays country borders. +3. Tap the play button to start a navigation + +[![MapLibre Logo](.github/navigation.png)](https://maplibre.org) ## Community @@ -162,3 +63,4 @@ Code is [licensed](LICENSE.md) under MIT and ISC. ISC is meant to be functionally equivalent to the MIT license. Copyright (c) 2022 MapLibre contributors +®®® \ No newline at end of file From 3dd0e4ffecd525d9e745337dc20e1b5888c14cb3 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Mon, 10 Jun 2024 11:00:48 +0200 Subject: [PATCH 31/44] address PR comments --- Example/example/SceneDelegate.swift | 9 +-- .../NavigationViewController.swift | 63 +++++-------------- MapboxNavigation/RouteMapViewController.swift | 8 +-- 3 files changed, 19 insertions(+), 61 deletions(-) diff --git a/Example/example/SceneDelegate.swift b/Example/example/SceneDelegate.swift index b81f1836e..39603c5f7 100644 --- a/Example/example/SceneDelegate.swift +++ b/Example/example/SceneDelegate.swift @@ -72,7 +72,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } extension SceneDelegate: NavigationViewControllerDelegate { - func navigationViewControllerDidFinish(_ navigationViewController: NavigationViewController) { + func navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController) { navigationViewController.endNavigation() } } @@ -101,13 +101,8 @@ private extension SceneDelegate { @objc func cameraButtonTapped() { guard let waypoint = self.waypoints.randomElement() else { return } - - func randomCLLocationDistance(min: CLLocationDistance, max: CLLocationDistance) -> CLLocationDistance { - CLLocationDistance(arc4random_uniform(UInt32(max - min)) + UInt32(min)) - } - let distance = randomCLLocationDistance(min: 10, max: 100_000) - + let distance = CLLocationDistance.random(in: 10 ... 100_000) self.viewController.mapView.camera = .init(lookingAtCenter: waypoint.coordinate, acrossDistance: distance, pitch: 0, diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 663146137..84c6abb8e 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -11,14 +11,6 @@ import CarPlay */ @objc(MBNavigationViewControllerDelegate) public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { - /** - Called when the navigation view controller is dismissed, such as when the user ends a trip. - - - parameter navigationViewController: The navigation view controller that was dismissed. - - parameter canceled: True if the user dismissed the navigation view controller by tapping the Cancel button; false if the navigation view controller dismissed by some other means. - */ - @objc optional func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) - /** Called when user arrived at the destination of the trip. @@ -313,9 +305,12 @@ open class NavigationViewController: UIViewController { /** Bool which should be set to true if a CarPlayNavigationView is also being used. */ - public var isUsedInConjunctionWithCarPlayWindow = false { - didSet { - self.mapViewController.isUsedInConjunctionWithCarPlayWindow = self.isUsedInConjunctionWithCarPlayWindow + public var isUsedInConjunctionWithCarPlayWindow: Bool { + get { + self.mapViewController.isUsedInConjunctionWithCarPlayWindow + } + set { + self.mapViewController.isUsedInConjunctionWithCarPlayWindow = newValue } } @@ -333,39 +328,6 @@ open class NavigationViewController: UIViewController { super.init(coder: aDecoder) self.mapView.delegate = self } - - /// Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. - /// - /// ``` - /// let dayStyle = DayStyle(mapStyleURL: styleURL) - /// let vc = NavigationViewController(route: route, styles: [dayStyle]) - /// self.presentViewController(vc, animated: true) - /// ``` - /// - Parameters: - /// - route: The route to follow. - /// - directions: Used when recomputing a new route, for example if the user takes a wrong turn and needs re-routing. If unspecified, a default will be used. - /// - styles: The `[dayStyle]` or `[dayStyle, nightStyle]` styles used to render the map. If nil, the default styles will be used. - /// - routeController: Used to monitor the route and notify of changes to the route. If nil, a default will be used. - /// - locationManager: Tracks the users location along the route. If nil, a default will be used. - /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. - /// - /// See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. - @available(*, deprecated, message: "Use `init(for:dayStyle:...) or init(for:dayStyleURL:...)` instead.") - @objc(initWithRoute:directions:styles:routeController:locationManager:voiceController:) - public convenience init(for route: Route, - directions: Directions = Directions.shared, - styles: [Style]? = [DayStyle(), NightStyle()], - routeController: RouteController? = nil, - locationManager: NavigationLocationManager = NavigationLocationManager(), - voiceController: RouteVoiceController = RouteVoiceController()) { - let styles = styles ?? [] - assert(styles.count <= 2, "Having more than two styles is undefined.") - let dayStyle = styles.first ?? DayStyle(demoStyle: ()) - let nightStyle = styles.count > 1 ? styles[1] : NightStyle(mapStyleURL: dayStyle.mapStyleURL) - - self.init(dayStyle: dayStyle, nightStyle: nightStyle, directions: directions, voiceController: voiceController) - self.startNavigation(with: route, locationManager: locationManager) - } /// Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. /// @@ -379,12 +341,10 @@ open class NavigationViewController: UIViewController { /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. /// /// See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. - @objc(initWithDayStyleURL:nightStyleURL:directions:routeController:locationManager:voiceController:) + @objc(initWithDayStyleURL:nightStyleURL:directions:voiceController:) public convenience init(dayStyleURL: URL, nightStyleURL: URL? = nil, directions: Directions = Directions.shared, - routeController: RouteController? = nil, - locationManager: NavigationLocationManager? = nil, voiceController: RouteVoiceController = RouteVoiceController()) { let dayStyle = DayStyle(mapStyleURL: dayStyleURL) let nightStyle = NightStyle(mapStyleURL: nightStyleURL ?? dayStyleURL) @@ -461,8 +421,13 @@ open class NavigationViewController: UIViewController { // MARK: - NavigationViewController - public func startNavigation(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager = NavigationLocationManager()) { - self.locationManager = locationManager + public func startNavigation(with route: Route, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) { + if let locationManager { + self.locationManager = locationManager + } + if let routeController { + self.routeController = routeController + } self.route = route self.mapViewController.navigationView.showUI(animated: true) diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 4de021bcc..42888beb7 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -205,20 +205,18 @@ class RouteMapViewController: UIViewController { // MARK: - RouteMapViewController func prepareForNavigation() { + guard let routeController else { return } + self.resetETATimer() - self.navigationView.muteButton.isSelected = NavigationSettings.shared.voiceMuted self.mapView.compassView.isHidden = true - - self.mapView.tracksUserCourse = self.route != nil + self.mapView.tracksUserCourse = true if let camera = self.pendingCamera { self.mapView.camera = camera return } - guard let routeController else { return } - if let location = routeController.location, location.course > 0 { self.mapView.updateCourseTracking(location: location, animated: false) From fa8fbbc58ed1c3a262c189ee085eab4938cb624c Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 11 Jun 2024 14:35:05 +0200 Subject: [PATCH 32/44] improve documentation --- .../NavigationViewController.swift | 44 +++++++------------ README.md | 43 ++++++++++++++++++ 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 84c6abb8e..c9ddb4c3c 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -298,7 +298,7 @@ open class NavigationViewController: UIViewController { } /** - If `true`, `UIApplication.isIdleTimerDisabled` is set to `true` in `viewWillAppear(_:)` and `false` in `viewWillDisappear(_:)`. If your application manages the idle timer itself, set this property to `false`. + If `true`, `UIApplication.isIdleTimerDisabled` is set to `true` while a navigation is running. If your application manages the idle timer itself, set this property to `false`. */ public var shouldManageApplicationIdleTimer = true @@ -321,42 +321,29 @@ open class NavigationViewController: UIViewController { // MARK: - Lifecycle - public required init?(coder aDecoder: NSCoder) { - self.directions = .shared - self.mapViewController = RouteMapViewController(routeController: self.routeController) - self.locationManager = NavigationLocationManager() - super.init(coder: aDecoder) - self.mapView.delegate = self - } - - /// Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. - /// + /// Initializes a `NavigationViewController` that displays a map with a given style. /// - Parameters: - /// - route: The route to follow. /// - dayStyleURL: URL for the style rules used to render the map during daylight hours. /// - nightStyleURL: URL for the style rules used to render the map during nighttime hours. If nil, `dayStyleURL` will be used at night as well. /// - directions: Used when recomputing a new route, for example if the user takes a wrong turn and needs re-routing. If unspecified, a default will be used. - /// - routeController: Used to monitor the route and notify of changes to the route. If nil, a default will be used. - /// - locationManager: Tracks the users location along the route. If nil, a default will be used. /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. - /// - /// See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. @objc(initWithDayStyleURL:nightStyleURL:directions:voiceController:) public convenience init(dayStyleURL: URL, nightStyleURL: URL? = nil, - directions: Directions = Directions.shared, + directions: Directions = .shared, voiceController: RouteVoiceController = RouteVoiceController()) { let dayStyle = DayStyle(mapStyleURL: dayStyleURL) let nightStyle = NightStyle(mapStyleURL: nightStyleURL ?? dayStyleURL) self.init(dayStyle: dayStyle, nightStyle: nightStyle, directions: directions, voiceController: voiceController) } - - /** - Initializes a `NavigationViewController` that provides turn by turn navigation for the given route. A optional `direction` object is needed for potential rerouting. - See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. - */ - @objc(initWithStyleURL:directions:styles:voiceController:) + /// Initializes a `NavigationViewController` that displays a map with a given style. + /// - Parameters: + /// - dayStyle: Style used to render the map during daylight hours. + /// - nightStyle: Style used to render the map during nighttime hours. If nil, `dayStyle` will be used at night as well. + /// - directions: Used when recomputing a new route, for example if the user takes a wrong turn and needs re-routing. If unspecified, a default will be used. + /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. + /// @objc(initWithStyleURL:directions:styles:voiceController:) public required init(dayStyle: Style, nightStyle: Style? = nil, directions: Directions = Directions.shared, @@ -395,9 +382,10 @@ open class NavigationViewController: UIViewController { self.mapViewController.navigationView.hideUI(animated: false) self.mapView.tracksUserCourse = false } - - convenience init() { - self.init(dayStyle: Style(demoStyle: ())) + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } deinit { @@ -412,9 +400,7 @@ open class NavigationViewController: UIViewController { override open func viewDidLoad() { super.viewDidLoad() - // Initialize voice controller if it hasn't been overridden. - // This is optional and lazy so it can be mutated by the developer after init. - _ = self.voiceController + self.resumeNotifications() self.view.clipsToBounds = true } diff --git a/README.md b/README.md index d13110e46..c27e29d71 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,49 @@ All issues are covered with this SDK. - Transitioned from the [Mapbox SDK](https://github.com/mapbox/mapbox-gl-native-ios) (version 4.3) to [Maplibre Maps SDK](https://github.com/maplibre/maplibre-gl-native) (version 6.0.0) - Added optional config parameter in NavigationMapView constructor to customize certain properties like route line color +# Migrating from v2 to v3 + +Maplibre v3 allows you to start a navigation in an existing Map, so no modal ViewController needs to be presented over an existing map as before. This results in some breaking changes. + +### Step 1: + +Replace your ViewController that hosts the mapView with a `NavigationViewController`. We suggest to create a subclass of `NavigationViewController` and override init & call `super.init(dayStyle:)` or `super.init(dayStyleURL:)`. NavigationViewController will not do anything on its own until you start a navigation. + +### Step 2: + +Start the navigation by calling `startNavigation(with: route)`. If you want to simulate your route, you need to pass in the optional locationManager parameter, otherwise the real location of the device will be used. + +```swift +func locationManager(for route: Route) -> NavigationLocationManager { +#if targetEnvironment(simulator) + let locationManager = SimulatedLocationManager(route: route) + locationManager.speedMultiplier = 2 + return locationManager +#else + return NavigationLocationManager() +#endif +} + +self.startNavigation(with: route, locationManager: locationManager(for : route)) +``` + +### Step 3: + +Make your ViewController conform to `NavigationViewControllerDelegate` and implement `navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController)` to transition back to normal map mode. This delegate is called when the user arrives at the destination or cancels the navigation. + +```swift +extension SceneDelegate: NavigationViewControllerDelegate { + func navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController) { + navigationViewController.endNavigation() + } +} +``` + +### Backwards compatibility + +If for some reason, you want to keep the old way of presenting a navigation modally, you can still do that. Simply call `startNavigation(with: route)` right after creating the `NavigationViewController`. + + # Getting Started If you are looking to include this inside your project, you have to follow the the following steps: From 39a388b37e194f62fa87f4f40dd05a5838860f00 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 11 Jun 2024 16:44:17 +0200 Subject: [PATCH 33/44] Add Changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba90f87e..8d72fa325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,21 @@ - Merged in - Fix: NavigationViewController displayed incorrect `speedMultiplier` when using SimulatedLocationManager - Merged in +- Start & Stop Navigation in existing Map + - Removed: `NavigationViewController(for route: Route, dayStyle: Style, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil, voiceController: RouteVoiceController? = nil)` use `NavigationViewController(dayStyleURL: URL, nightStyleURL: URL? = nil,directions: Directions = .shared, voiceController: RouteVoiceController = RouteVoiceController())` followed by `startNavigation(with route: Route)` instead. + - To simulate a route, pass a `SimulatedLocationManager` to `startNavigation()` function: + + ```swift + let vc = NavigationViewController(dayStyleURL: AppConfig().tileserverStyleUrl) + + if Env.current.simulateLocationForTesting { + let simulatedLocationManager = SimulatedLocationManager(route: route) + simulatedLocationManager.speedMultiplier = 5 + vc.startNavigation(with: route, locationManager: simulatedLocationManager) + } else { + vc.startNavigation(with: route) + } + ``` ## v2.0.0 (May 23, 2023) - Upgrade minimum iOS version from 11.0 to 12.0. From 7bf9be47b91354d251ad705d5b4d787a79e0607b Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 11 Jun 2024 16:47:16 +0200 Subject: [PATCH 34/44] fix tests --- MapboxNavigation/NavigationViewController.swift | 2 +- .../Sources/Tests/NavigationViewControllerTests.swift | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index c9ddb4c3c..a1ae46b7a 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -343,7 +343,7 @@ open class NavigationViewController: UIViewController { /// - nightStyle: Style used to render the map during nighttime hours. If nil, `dayStyle` will be used at night as well. /// - directions: Used when recomputing a new route, for example if the user takes a wrong turn and needs re-routing. If unspecified, a default will be used. /// - voiceController: Produces voice instructions for route navigation. If nil, a default will be used. - /// @objc(initWithStyleURL:directions:styles:voiceController:) + @objc(initWithDayStyle:nightStyle:directions:voiceController:) public required init(dayStyle: Style, nightStyle: Style? = nil, directions: Directions = Directions.shared, diff --git a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift index 743baf52b..6f8bcef86 100644 --- a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift @@ -271,13 +271,9 @@ class NavigationViewControllerTestable: NavigationViewController { func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) { self.styleLoadedExpectation.fulfill() } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("This initalizer is not supported in this testing subclass.") - } - @objc(initWithStyleURL:directions:styles:voiceController:) required init(dayStyle: Style, nightStyle: Style? = nil, directions: Directions = Directions.shared, voiceController: RouteVoiceController = RouteVoiceController()) { + @objc(initWithDayStyle:nightStyle:directions:voiceController:) + required init(dayStyle: Style, nightStyle: Style? = nil, directions: Directions = Directions.shared, voiceController: RouteVoiceController = RouteVoiceController()) { fatalError("init(dayStyle:nightStyle:directions:voiceController:) has not been implemented") } } From adc5d913a8a22f6beb93b9ed228702bafae50900 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 12 Jun 2024 16:24:33 +0200 Subject: [PATCH 35/44] fix indent --- CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d72fa325..7f849b088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,15 +30,15 @@ - To simulate a route, pass a `SimulatedLocationManager` to `startNavigation()` function: ```swift - let vc = NavigationViewController(dayStyleURL: AppConfig().tileserverStyleUrl) - - if Env.current.simulateLocationForTesting { - let simulatedLocationManager = SimulatedLocationManager(route: route) + let vc = NavigationViewController(dayStyleURL: AppConfig().tileserverStyleUrl) + + if Env.current.simulateLocationForTesting { + let simulatedLocationManager = SimulatedLocationManager(route: route) simulatedLocationManager.speedMultiplier = 5 - vc.startNavigation(with: route, locationManager: simulatedLocationManager) - } else { - vc.startNavigation(with: route) - } + vc.startNavigation(with: route, locationManager: simulatedLocationManager) + } else { + vc.startNavigation(with: route) + } ``` ## v2.0.0 (May 23, 2023) From 20cd030d226a05e42354e59cdf9aedc3fd923ad4 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 12 Jun 2024 16:26:02 +0200 Subject: [PATCH 36/44] Update README.md Co-authored-by: Michael Kirk --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c27e29d71..3c289f84f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ All issues are covered with this SDK. # Migrating from v2 to v3 -Maplibre v3 allows you to start a navigation in an existing Map, so no modal ViewController needs to be presented over an existing map as before. This results in some breaking changes. +MaplibreNavigation v3 allows you to start a navigation in an existing Map, so no modal ViewController needs to be presented over an existing map as before. This results in some breaking changes. ### Step 1: From fdad578242f323e674d56d321b017dff18af4360 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 12 Jun 2024 16:26:27 +0200 Subject: [PATCH 37/44] Update README.md Co-authored-by: Michael Kirk --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c289f84f..ab5ac97a0 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ MaplibreNavigation v3 allows you to start a navigation in an existing Map, so no ### Step 1: -Replace your ViewController that hosts the mapView with a `NavigationViewController`. We suggest to create a subclass of `NavigationViewController` and override init & call `super.init(dayStyle:)` or `super.init(dayStyleURL:)`. NavigationViewController will not do anything on its own until you start a navigation. +Replace your ViewController that hosts the mapView with a `NavigationViewController`. We suggest to create a subclass of `NavigationViewController` and override init & call `super.init(dayStyle:)` or `super.init(dayStyleURL:)`. With v3, NavigationViewController will not start navigation until you request it. ### Step 2: From 335688a1340ac8dc47ad5eb3d3423c1e59e381aa Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 12 Jun 2024 16:28:55 +0200 Subject: [PATCH 38/44] update readme --- README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ab5ac97a0..57c59a322 100644 --- a/README.md +++ b/README.md @@ -31,17 +31,13 @@ Replace your ViewController that hosts the mapView with a `NavigationViewControl Start the navigation by calling `startNavigation(with: route)`. If you want to simulate your route, you need to pass in the optional locationManager parameter, otherwise the real location of the device will be used. ```swift -func locationManager(for route: Route) -> NavigationLocationManager { -#if targetEnvironment(simulator) - let locationManager = SimulatedLocationManager(route: route) - locationManager.speedMultiplier = 2 - return locationManager +#if targetEnvironment(simulator) + let locationManager = SimulatedLocationManager(route: route) + locationManager.speedMultiplier = 2 + self.startNavigation(with: route, locationManager: locationManager) #else - return NavigationLocationManager() + self.startNavigation(with: route) #endif -} - -self.startNavigation(with: route, locationManager: locationManager(for : route)) ``` ### Step 3: From 7a0d04e714720175a24c09252ed1a6004e2d20b3 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 12 Jun 2024 16:32:59 +0200 Subject: [PATCH 39/44] remove duplicated changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f849b088..a0cb65cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changes to the Mapbox Navigation SDK for iOS ## Unreleased - - Map can be used without an active navigation, navigation can be started anytime with `startNavigation(route: Route)`. + - The `speak` method in `RouteVoiceController` can be used without a given `RouteProgress` or the `RouteProgress` can explicitly ignored so that it will not be added to the voice instruction. - `RouteProgress` is now optional in `willSpeak` method of `VoiceControllerDelegate` if the `RouteProgress` in the `speak` method of the `RouteVoiceController is `nil`. - Uses the `Locale` given in `RouteOptions` to create the corresponding `AVSpeechSynthesisVoice`. From 1998d53432487d48e2c852009a999816f2583b46 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 12 Jun 2024 16:34:57 +0200 Subject: [PATCH 40/44] update readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 57c59a322..c32441481 100644 --- a/README.md +++ b/README.md @@ -101,5 +101,4 @@ Read the [CONTRIBUTING.md](CONTRIBUTING.md) guide in order to get familiar with Code is [licensed](LICENSE.md) under MIT and ISC. ISC is meant to be functionally equivalent to the MIT license. -Copyright (c) 2022 MapLibre contributors -®®® \ No newline at end of file +Copyright (c) 2022 MapLibre contributors \ No newline at end of file From 9cc42ff3390709cd640b89c6b93cd0d1122dcb24 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 13 Jun 2024 15:58:09 +0200 Subject: [PATCH 41/44] Update README.md Co-authored-by: Ian Wagner --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c32441481..49c5eb0fe 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ extension SceneDelegate: NavigationViewControllerDelegate { ### Backwards compatibility -If for some reason, you want to keep the old way of presenting a navigation modally, you can still do that. Simply call `startNavigation(with: route)` right after creating the `NavigationViewController`. +If you want to keep the old way of presenting a navigation modally, you can still do that. Simply call `startNavigation(with: route)` right after creating the `NavigationViewController`. # Getting Started From 0c6734e8aed2426246bdc98ed98ff9685585b821 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Thu, 13 Jun 2024 16:03:38 +0200 Subject: [PATCH 42/44] clarify removal of delegate method --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 49c5eb0fe..69afaeb14 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,12 @@ extension SceneDelegate: NavigationViewControllerDelegate { } ``` +NOTE: You've probably used this function previously, it was removed as we don't dismiss a ViewController anymore. + +```swift +@objc optional func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) +``` + ### Backwards compatibility If you want to keep the old way of presenting a navigation modally, you can still do that. Simply call `startNavigation(with: route)` right after creating the `NavigationViewController`. From fb2197aa6e45bce7b2101bbbaaa02915eb67ee5a Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Tue, 18 Jun 2024 22:01:31 +0200 Subject: [PATCH 43/44] forward mapView delegate methods --- .../NavigationViewController.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index a1ae46b7a..a2a035c18 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -17,6 +17,14 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { - parameter navigationViewController: The navigation view controller that finished navigation. */ @objc optional func navigationViewControllerDidFinishRouting(_ navigationViewController: NavigationViewController) + + /** + Called when the underlaying mapView finished loading the style. + + - parameter navigationViewController: The navigation view controller that finished navigation. + - parameter style: The applied style + */ + @objc optional func navigationViewController(_ navigationViewController: NavigationViewController, didFinishLoading style: MLNStyle) /** Called when the user arrives at the destination waypoint for a route leg. @@ -135,6 +143,13 @@ public protocol NavigationViewControllerDelegate: VisualInstructionDelegate { */ @objc(navigationViewController:didSelectRoute:) optional func navigationViewController(_ navigationViewController: NavigationViewController, didSelect route: Route) + + /** + Called when the user taps to select an annotation on the navigation view controller’s map view. + - parameter navigationViewController: The navigation view controller presenting the route that the user selected. + - parameter annotation: The annotation on the map that the user selected. + */ + @objc optional func navigationViewController(_ navigationViewController: NavigationViewController, didSelect annotation: MLNAnnotation) /** Return an `MLNAnnotationImage` that represents the destination marker. @@ -505,6 +520,14 @@ open class NavigationViewController: UIViewController { // MARK: - RouteMapViewControllerDelegate extension NavigationViewController: RouteMapViewControllerDelegate { + public func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) { + self.delegate?.navigationViewController?(self, didFinishLoading: style) + } + + public func mapView(_ mapView: MLNMapView, didSelect annotation: any MLNAnnotation) { + self.delegate?.navigationViewController?(self, didSelect: annotation) + } + public func navigationMapView(_ mapView: NavigationMapView, routeCasingStyleLayerWithIdentifier identifier: String, source: MLNSource) -> MLNStyleLayer? { self.delegate?.navigationViewController?(self, routeCasingStyleLayerWithIdentifier: identifier, source: source) } From 9ccb7b852a2f22e367773b6c4b4472dd93dfd5e4 Mon Sep 17 00:00:00 2001 From: Patrick Kladek Date: Wed, 19 Jun 2024 13:39:14 +0200 Subject: [PATCH 44/44] fix tests --- .../Sources/Tests/NavigationViewControllerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift index 6f8bcef86..b6ec64eac 100644 --- a/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/Sources/Tests/NavigationViewControllerTests.swift @@ -268,7 +268,7 @@ class NavigationViewControllerTestable: NavigationViewController { fatalError("init(for:directions:dayStyle:nightStyle:routeController:locationManager:voiceController:) has not been implemented") } - func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) { + override func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) { self.styleLoadedExpectation.fulfill() }