diff --git a/README.md b/README.md new file mode 100644 index 0000000..643ecab --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +## SantaClaraCams + +SantaClaraCams is an iOS app that provides an easy way to view the traffic cameras provided by [Santa Clara City](http://santaclaraca.gov/residents/maps/traffic-cameras) (California). Select a camera from the table view, or interactive map, to view the current stream. + +### Screenshots + +![First view](Screenshots/table_view.png) + +![San Tomas Aquino Creek Trail camera with map zoomed out](Screenshots/stact_out.png) + +![San Tomas Aquino Creek Trail camera with map zoomed in](Screenshots/stact_in.png) + +![Map zoomed out to show many cameras](Screenshots/map_spread.png) + +![Intersection of Great America and Bunker hill](Screenshots/gabh_in.png) diff --git a/SantaClaraCams.xcodeproj/project.pbxproj b/SantaClaraCams.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e584738 --- /dev/null +++ b/SantaClaraCams.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + FA0D3E3422469FE8007724A0 /* SCCAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = FA0D3E3322469FE8007724A0 /* SCCAppDelegate.m */; }; + FA0D3E3722469FE8007724A0 /* SCCViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FA0D3E3622469FE8007724A0 /* SCCViewController.m */; }; + FA0D3E3A22469FE8007724A0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA0D3E3822469FE8007724A0 /* Main.storyboard */; }; + FA0D3E3C22469FE9007724A0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA0D3E3B22469FE9007724A0 /* Assets.xcassets */; }; + FA0D3E3F22469FE9007724A0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA0D3E3D22469FE9007724A0 /* LaunchScreen.storyboard */; }; + FA0D3E4222469FE9007724A0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FA0D3E4122469FE9007724A0 /* main.m */; }; + FA0D3E552246A1CB007724A0 /* SantaClaraCityCams.h in Headers */ = {isa = PBXBuildFile; fileRef = FA0D3E532246A1CB007724A0 /* SantaClaraCityCams.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FA0D3E582246A1CB007724A0 /* SantaClaraCityCams.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA0D3E512246A1CB007724A0 /* SantaClaraCityCams.framework */; }; + FA0D3E592246A1CB007724A0 /* SantaClaraCityCams.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FA0D3E512246A1CB007724A0 /* SantaClaraCityCams.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FA0D3E652246C2AE007724A0 /* SCCCameraLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = FA0D3E632246C2AE007724A0 /* SCCCameraLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FA0D3E662246C2AE007724A0 /* SCCCameraLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = FA0D3E642246C2AE007724A0 /* SCCCameraLocation.m */; }; + FA0D3E6A2246D0DC007724A0 /* SCCLocationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FA0D3E692246D0DC007724A0 /* SCCLocationViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + FA0D3E562246A1CB007724A0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FA0D3E2722469FE8007724A0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = FA0D3E502246A1CB007724A0; + remoteInfo = SantaClaraCityCams; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + FA0D3E5D2246A1CB007724A0 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + FA0D3E592246A1CB007724A0 /* SantaClaraCityCams.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + FA0D3E2F22469FE8007724A0 /* SantaClaraCams.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SantaClaraCams.app; sourceTree = BUILT_PRODUCTS_DIR; }; + FA0D3E3222469FE8007724A0 /* SCCAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCCAppDelegate.h; sourceTree = ""; }; + FA0D3E3322469FE8007724A0 /* SCCAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCCAppDelegate.m; sourceTree = ""; }; + FA0D3E3522469FE8007724A0 /* SCCViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCCViewController.h; sourceTree = ""; }; + FA0D3E3622469FE8007724A0 /* SCCViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCCViewController.m; sourceTree = ""; }; + FA0D3E3922469FE8007724A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + FA0D3E3B22469FE9007724A0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + FA0D3E3E22469FE9007724A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + FA0D3E4022469FE9007724A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FA0D3E4122469FE9007724A0 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + FA0D3E512246A1CB007724A0 /* SantaClaraCityCams.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SantaClaraCityCams.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FA0D3E532246A1CB007724A0 /* SantaClaraCityCams.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SantaClaraCityCams.h; sourceTree = ""; }; + FA0D3E542246A1CB007724A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FA0D3E632246C2AE007724A0 /* SCCCameraLocation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCCCameraLocation.h; sourceTree = ""; }; + FA0D3E642246C2AE007724A0 /* SCCCameraLocation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCCCameraLocation.m; sourceTree = ""; }; + FA0D3E682246D0DC007724A0 /* SCCLocationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCCLocationViewController.h; sourceTree = ""; }; + FA0D3E692246D0DC007724A0 /* SCCLocationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCCLocationViewController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + FA0D3E2C22469FE8007724A0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FA0D3E582246A1CB007724A0 /* SantaClaraCityCams.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FA0D3E4E2246A1CB007724A0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + FA0D3E2622469FE8007724A0 = { + isa = PBXGroup; + children = ( + FA0D3E3122469FE8007724A0 /* SantaClaraCams */, + FA0D3E522246A1CB007724A0 /* SantaClaraCityCams */, + FA0D3E3022469FE8007724A0 /* Products */, + ); + sourceTree = ""; + }; + FA0D3E3022469FE8007724A0 /* Products */ = { + isa = PBXGroup; + children = ( + FA0D3E2F22469FE8007724A0 /* SantaClaraCams.app */, + FA0D3E512246A1CB007724A0 /* SantaClaraCityCams.framework */, + ); + name = Products; + sourceTree = ""; + }; + FA0D3E3122469FE8007724A0 /* SantaClaraCams */ = { + isa = PBXGroup; + children = ( + FA0D3E4B2246A0A9007724A0 /* AppDelegate */, + FA0D3E4A2246A09D007724A0 /* ViewControllers */, + FA0D3E492246A08F007724A0 /* Storyboards */, + FA0D3E482246A063007724A0 /* Supporting */, + ); + path = SantaClaraCams; + sourceTree = ""; + }; + FA0D3E482246A063007724A0 /* Supporting */ = { + isa = PBXGroup; + children = ( + FA0D3E3B22469FE9007724A0 /* Assets.xcassets */, + FA0D3E4122469FE9007724A0 /* main.m */, + FA0D3E4022469FE9007724A0 /* Info.plist */, + ); + name = Supporting; + sourceTree = ""; + }; + FA0D3E492246A08F007724A0 /* Storyboards */ = { + isa = PBXGroup; + children = ( + FA0D3E3822469FE8007724A0 /* Main.storyboard */, + FA0D3E3D22469FE9007724A0 /* LaunchScreen.storyboard */, + ); + name = Storyboards; + sourceTree = ""; + }; + FA0D3E4A2246A09D007724A0 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + FA0D3E3522469FE8007724A0 /* SCCViewController.h */, + FA0D3E3622469FE8007724A0 /* SCCViewController.m */, + FA0D3E682246D0DC007724A0 /* SCCLocationViewController.h */, + FA0D3E692246D0DC007724A0 /* SCCLocationViewController.m */, + ); + path = ViewControllers; + sourceTree = ""; + }; + FA0D3E4B2246A0A9007724A0 /* AppDelegate */ = { + isa = PBXGroup; + children = ( + FA0D3E3222469FE8007724A0 /* SCCAppDelegate.h */, + FA0D3E3322469FE8007724A0 /* SCCAppDelegate.m */, + ); + path = AppDelegate; + sourceTree = ""; + }; + FA0D3E522246A1CB007724A0 /* SantaClaraCityCams */ = { + isa = PBXGroup; + children = ( + FA0D3E532246A1CB007724A0 /* SantaClaraCityCams.h */, + FA0D3E672246C2BC007724A0 /* Models */, + FA0D3E542246A1CB007724A0 /* Info.plist */, + ); + path = SantaClaraCityCams; + sourceTree = ""; + }; + FA0D3E672246C2BC007724A0 /* Models */ = { + isa = PBXGroup; + children = ( + FA0D3E632246C2AE007724A0 /* SCCCameraLocation.h */, + FA0D3E642246C2AE007724A0 /* SCCCameraLocation.m */, + ); + path = Models; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + FA0D3E4C2246A1CB007724A0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + FA0D3E552246A1CB007724A0 /* SantaClaraCityCams.h in Headers */, + FA0D3E652246C2AE007724A0 /* SCCCameraLocation.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + FA0D3E2E22469FE8007724A0 /* SantaClaraCams */ = { + isa = PBXNativeTarget; + buildConfigurationList = FA0D3E4522469FE9007724A0 /* Build configuration list for PBXNativeTarget "SantaClaraCams" */; + buildPhases = ( + FA0D3E2B22469FE8007724A0 /* Sources */, + FA0D3E2C22469FE8007724A0 /* Frameworks */, + FA0D3E2D22469FE8007724A0 /* Resources */, + FA0D3E5D2246A1CB007724A0 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + FA0D3E572246A1CB007724A0 /* PBXTargetDependency */, + ); + name = SantaClaraCams; + productName = SantaClaraCams; + productReference = FA0D3E2F22469FE8007724A0 /* SantaClaraCams.app */; + productType = "com.apple.product-type.application"; + }; + FA0D3E502246A1CB007724A0 /* SantaClaraCityCams */ = { + isa = PBXNativeTarget; + buildConfigurationList = FA0D3E5A2246A1CB007724A0 /* Build configuration list for PBXNativeTarget "SantaClaraCityCams" */; + buildPhases = ( + FA0D3E4C2246A1CB007724A0 /* Headers */, + FA0D3E4D2246A1CB007724A0 /* Sources */, + FA0D3E4E2246A1CB007724A0 /* Frameworks */, + FA0D3E4F2246A1CB007724A0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SantaClaraCityCams; + productName = SantaClaraCityCams; + productReference = FA0D3E512246A1CB007724A0 /* SantaClaraCityCams.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + FA0D3E2722469FE8007724A0 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = SCC; + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = Leptos; + TargetAttributes = { + FA0D3E2E22469FE8007724A0 = { + CreatedOnToolsVersion = 10.1; + }; + FA0D3E502246A1CB007724A0 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = FA0D3E2A22469FE8007724A0 /* Build configuration list for PBXProject "SantaClaraCams" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = FA0D3E2622469FE8007724A0; + productRefGroup = FA0D3E3022469FE8007724A0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + FA0D3E2E22469FE8007724A0 /* SantaClaraCams */, + FA0D3E502246A1CB007724A0 /* SantaClaraCityCams */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + FA0D3E2D22469FE8007724A0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FA0D3E3F22469FE9007724A0 /* LaunchScreen.storyboard in Resources */, + FA0D3E3C22469FE9007724A0 /* Assets.xcassets in Resources */, + FA0D3E3A22469FE8007724A0 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FA0D3E4F2246A1CB007724A0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + FA0D3E2B22469FE8007724A0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FA0D3E3722469FE8007724A0 /* SCCViewController.m in Sources */, + FA0D3E4222469FE9007724A0 /* main.m in Sources */, + FA0D3E3422469FE8007724A0 /* SCCAppDelegate.m in Sources */, + FA0D3E6A2246D0DC007724A0 /* SCCLocationViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FA0D3E4D2246A1CB007724A0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FA0D3E662246C2AE007724A0 /* SCCCameraLocation.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + FA0D3E572246A1CB007724A0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FA0D3E502246A1CB007724A0 /* SantaClaraCityCams */; + targetProxy = FA0D3E562246A1CB007724A0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + FA0D3E3822469FE8007724A0 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + FA0D3E3922469FE8007724A0 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + FA0D3E3D22469FE9007724A0 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + FA0D3E3E22469FE9007724A0 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + FA0D3E4322469FE9007724A0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + FA0D3E4422469FE9007724A0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + FA0D3E4622469FE9007724A0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7P56K8K4MY; + INFOPLIST_FILE = SantaClaraCams/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = null.leptos.SantaClaraCams; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FA0D3E4722469FE9007724A0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7P56K8K4MY; + INFOPLIST_FILE = SantaClaraCams/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = null.leptos.SantaClaraCams; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FA0D3E5B2246A1CB007724A0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 7P56K8K4MY; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/SantaClaraCityCams/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = null.leptos.SantaClaraCityCams; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + FA0D3E5C2246A1CB007724A0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 7P56K8K4MY; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/SantaClaraCityCams/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = null.leptos.SantaClaraCityCams; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + FA0D3E2A22469FE8007724A0 /* Build configuration list for PBXProject "SantaClaraCams" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FA0D3E4322469FE9007724A0 /* Debug */, + FA0D3E4422469FE9007724A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FA0D3E4522469FE9007724A0 /* Build configuration list for PBXNativeTarget "SantaClaraCams" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FA0D3E4622469FE9007724A0 /* Debug */, + FA0D3E4722469FE9007724A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FA0D3E5A2246A1CB007724A0 /* Build configuration list for PBXNativeTarget "SantaClaraCityCams" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FA0D3E5B2246A1CB007724A0 /* Debug */, + FA0D3E5C2246A1CB007724A0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = FA0D3E2722469FE8007724A0 /* Project object */; +} diff --git a/SantaClaraCams.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SantaClaraCams.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..a60595a --- /dev/null +++ b/SantaClaraCams.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SantaClaraCams/AppDelegate/SCCAppDelegate.h b/SantaClaraCams/AppDelegate/SCCAppDelegate.h new file mode 100644 index 0000000..5de6f5f --- /dev/null +++ b/SantaClaraCams/AppDelegate/SCCAppDelegate.h @@ -0,0 +1,15 @@ +// +// SCCAppDelegate.h +// SantaClaraCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +@import UIKit; + +@interface SCCAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/SantaClaraCams/AppDelegate/SCCAppDelegate.m b/SantaClaraCams/AppDelegate/SCCAppDelegate.m new file mode 100644 index 0000000..357f593 --- /dev/null +++ b/SantaClaraCams/AppDelegate/SCCAppDelegate.m @@ -0,0 +1,46 @@ +// +// SCCAppDelegate.m +// SantaClaraCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +#import "SCCAppDelegate.h" + +@implementation SCCAppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. + // This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) + // or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. + // Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, + // and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the active state; + // here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. + // If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Contents.json b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..8d8f4bb --- /dev/null +++ b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "ItunesArtwork@2x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..4d48ec9 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..86248e8 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..ce10747 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..818ce4c Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..8798581 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..65dec5d Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..86248e8 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..a5a1501 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..6a997c6 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..6a997c6 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..2cbbcee Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..3f5e54b Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..9de8fdf Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..7868a61 Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 0000000..072d80a Binary files /dev/null and b/SantaClaraCams/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/SantaClaraCams/Assets.xcassets/Contents.json b/SantaClaraCams/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/SantaClaraCams/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SantaClaraCams/Base.lproj/LaunchScreen.storyboard b/SantaClaraCams/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..73116b7 --- /dev/null +++ b/SantaClaraCams/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SantaClaraCams/Base.lproj/Main.storyboard b/SantaClaraCams/Base.lproj/Main.storyboard new file mode 100644 index 0000000..4028776 --- /dev/null +++ b/SantaClaraCams/Base.lproj/Main.storyboard @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SantaClaraCams/Info.plist b/SantaClaraCams/Info.plist new file mode 100644 index 0000000..79cc0cb --- /dev/null +++ b/SantaClaraCams/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + SCC Cams + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SantaClaraCams/ViewControllers/SCCLocationViewController.h b/SantaClaraCams/ViewControllers/SCCLocationViewController.h new file mode 100644 index 0000000..4e0dc3b --- /dev/null +++ b/SantaClaraCams/ViewControllers/SCCLocationViewController.h @@ -0,0 +1,25 @@ +// +// SCCLocationViewController.h +// SantaClaraCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +@import UIKit; +@import MapKit; +@import SantaClaraCityCams; + +@interface SCCLocationViewController : UIViewController + +@property (strong, nonatomic) SCCCameraLocation *model; +@property (nonatomic) SCCFrameIndex currentFrame; + +@property (nonatomic) BOOL shouldShowDebugInfo; +@property (strong, nonatomic, readonly) NSString *debugInfo; + +@property (strong, nonatomic) IBOutlet UIImageView *imageView; +@property (strong, nonatomic) IBOutlet UILabel *debugLabel; +@property (strong, nonatomic) IBOutlet MKMapView *mapView; + +@end diff --git a/SantaClaraCams/ViewControllers/SCCLocationViewController.m b/SantaClaraCams/ViewControllers/SCCLocationViewController.m new file mode 100644 index 0000000..c011986 --- /dev/null +++ b/SantaClaraCams/ViewControllers/SCCLocationViewController.m @@ -0,0 +1,202 @@ +// +// SCCLocationViewController.m +// SantaClaraCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +#import "SCCLocationViewController.h" + +@implementation SCCLocationViewController { + NSTimer *_updateFrameTimer; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + CGSize currentViewSize = self.view.frame.size; + self.navigationController.navigationBarHidden = (currentViewSize.width > currentViewSize.height); + + for (SCCCameraLocation *location in SCCCameraLocation.knownLocations) { + [self.mapView addAnnotation:location]; + } + + [self _setupMapCameraForModel:self.model]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + /* I've been trying to figure out if a `_updateFrameTimer.valid` check is needed + * NSTimer is an abstract class, the concrete class is __NSCFTimer + * The implementation of -[__NSCFTimer invalidate] is { + * CFRunLoopTimerInvalidate(self); + * } + * the implementation of CFRunLoopTimerInvalidate can be found in CF/CFRunLoop.c + * the main code is only execute if (__CFIsValid(rlt)) + */ + [_updateFrameTimer invalidate]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + if (!_updateFrameTimer.valid) { + [self _setupFrameUpdateTimerForModel:self.model]; + } +} + +- (IBAction)_toggleDebugView:(UIGestureRecognizer *)recognizer { + if (recognizer.state == UIGestureRecognizerStateBegan) { + self.shouldShowDebugInfo ^= YES; + } +} + +- (IBAction)_imageDoubleTapSkip:(UIGestureRecognizer *)recognizer { + SCCFrameIndex const skipFrames = 5; + if (recognizer.state == UIGestureRecognizerStateEnded) { + CGPoint hitPoint = [recognizer locationInView:self.imageView]; + if (hitPoint.x > CGRectGetMidX(self.imageView.frame)) { + self.currentFrame += skipFrames; + } else { + self.currentFrame -= skipFrames; + } + } +} + +- (void)setModel:(SCCCameraLocation *)model { + _model = model; + + self.title = model.localizedName; + + [self _setupFrameUpdateTimerForModel:model]; + if (self.viewLoaded) { + [self _setupMapCameraForModel:model]; + } +} + +- (void)_setupMapCameraForModel:(SCCCameraLocation *)model { + if (model) { + BOOL shouldAnimate = (self.navigationController.visibleViewController == self); + /* these pitch and distance numbers are kind of estimated by what looked good */ + MKMapCamera *camera = [MKMapCamera cameraLookingAtCenterCoordinate:model.location.coordinate fromDistance:32 pitch:60 heading:model.heading]; + [self.mapView setCamera:camera animated:shouldAnimate]; + } +} + +- (void)_setupFrameUpdateTimerForModel:(SCCCameraLocation *)model { + [_updateFrameTimer invalidate]; + + if (model) { + __weak __typeof(self) weakself = self; + [model currentFrameWithCallback:^(SCCFrameIndex index, NSError *err) { + if (err) { + NSLog(@"Failed to find the current frame: %@", err); + } else if (index) { + weakself.currentFrame = index; + if (weakself) { + __typeof(self) strongself = weakself; + const NSTimeInterval secondsPerFrame = 1/model.framesPerSecond; + NSTimer *frameTimer = [NSTimer timerWithTimeInterval:secondsPerFrame repeats:YES block:^(NSTimer *timer) { + weakself.currentFrame++; + }]; + [NSRunLoop.mainRunLoop addTimer:frameTimer forMode:NSDefaultRunLoopMode]; + strongself->_updateFrameTimer = frameTimer; + } + } + }]; + } +} + +- (void)setDebugInfo:(NSString *)debugInfo { + _debugInfo = debugInfo; + [self _setDebugLabelTextAppropriately]; +} + +- (void)setShouldShowDebugInfo:(BOOL)shouldShowDebugInfo { + _shouldShowDebugInfo = shouldShowDebugInfo; + [self _setDebugLabelTextAppropriately]; +} + +- (void)_setDebugLabelTextAppropriately { + self.debugLabel.text = self.shouldShowDebugInfo ? self.debugInfo : nil; +} + +- (void)setCurrentFrame:(SCCFrameIndex)currentFrame { + // typically it's [0, upper), but this is (0, upper] + currentFrame %= self.model.maxFrame; + // `left ?:= value` would be a nice operation (assign `left` to `value` if `!left`) + if (currentFrame == 0) { + currentFrame = self.model.maxFrame; + } + _currentFrame = currentFrame; + + __weak __typeof(self) weakself = self; + NSURL *endpoint = [self.model urlForFrame:currentFrame]; + [[NSURLSession.sharedSession dataTaskWithURL:endpoint completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + UIImage *imageFrame = nil; + NSString *debugText = nil; + if (error) { + debugText = error.localizedDescription; + } else if (data) { + imageFrame = [UIImage imageWithData:data]; + + NSDate *frameDate = [SCCCameraLocation lastModifiedDateFromServerResponse:response error:&error]; + if (error) { + debugText = error.localizedDescription; + } else { + static NSDateFormatter *displayFormatter; + static dispatch_once_t createDisplayFormatterOnce; + dispatch_once(&createDisplayFormatterOnce, ^{ + displayFormatter = [NSDateFormatter new]; + displayFormatter.timeStyle = NSDateFormatterMediumStyle; + }); + + NSDate *nowTime = [NSDate date]; + debugText = [NSString stringWithFormat:@"" + "Key: %@\n" + "Now: %@\n" + "Frame: %@\n" + "Difference: %.4f\n" + "Index: %d", + weakself.model.key, [displayFormatter stringFromDate:nowTime], + [displayFormatter stringFromDate:frameDate], [nowTime timeIntervalSinceDate:frameDate], currentFrame]; + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + weakself.imageView.image = imageFrame; + weakself.debugInfo = debugText; + }); + }] resume]; +} + +- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { + if (self.model != view.annotation) { + self.model = view.annotation; + } +} + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + // in landscape, the camera goes full screen + // todo: only hide the bar after some time, and make it possible to still go back, possibly by making the bar reappear on click + [self.navigationController setNavigationBarHidden:(size.width > size.height) animated:YES]; +} + +- (void)dealloc { + [_updateFrameTimer invalidate]; +} +// either the map is visible, or the camera is full screen, either way, we don't want the home indicator +- (BOOL)prefersHomeIndicatorAutoHidden { + return YES; +} +// in landscape, the nav bar can be hidden, so the status bar should go away too +- (BOOL)prefersStatusBarHidden { + return YES; +} +// this is here, just in case +- (UIStatusBarStyle)preferredStatusBarStyle { + return UIStatusBarStyleLightContent; +} + +@end diff --git a/SantaClaraCams/ViewControllers/SCCViewController.h b/SantaClaraCams/ViewControllers/SCCViewController.h new file mode 100644 index 0000000..17a5188 --- /dev/null +++ b/SantaClaraCams/ViewControllers/SCCViewController.h @@ -0,0 +1,15 @@ +// +// SCCViewController.h +// SantaClaraCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +@import UIKit; +@import SantaClaraCityCams; + +@interface SCCViewController : UITableViewController + + +@end diff --git a/SantaClaraCams/ViewControllers/SCCViewController.m b/SantaClaraCams/ViewControllers/SCCViewController.m new file mode 100644 index 0000000..5e5d8b6 --- /dev/null +++ b/SantaClaraCams/ViewControllers/SCCViewController.m @@ -0,0 +1,48 @@ +// +// SCCViewController.m +// SantaClaraCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +#import "SCCViewController.h" +#import "SCCLocationViewController.h" + +@implementation SCCViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + // should this be [[UIView alloc] initWithFrame:CGRectNull] (or pass CGRectZero) ? + self.tableView.tableFooterView = [UIView new]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSParameterAssert(tableView == self.tableView); + NSParameterAssert(indexPath.section == 0); + + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CameraLocation"]; + cell.textLabel.text = SCCCameraLocation.knownLocations[indexPath.row].localizedName; + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + NSParameterAssert(tableView == self.tableView); + NSParameterAssert(indexPath.section == 0); + + SCCLocationViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"LocationController"]; + [controller loadViewIfNeeded]; + controller.model = SCCCameraLocation.knownLocations[indexPath.row]; + [self.navigationController pushViewController:controller animated:YES]; + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSParameterAssert(tableView == self.tableView); + NSParameterAssert(section == 0); + + return SCCCameraLocation.knownLocations.count; +} + +@end diff --git a/SantaClaraCams/main.m b/SantaClaraCams/main.m new file mode 100644 index 0000000..0e10e5f --- /dev/null +++ b/SantaClaraCams/main.m @@ -0,0 +1,15 @@ +// +// main.m +// SantaClaraCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +#import "AppDelegate/SCCAppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([SCCAppDelegate class])); + } +} diff --git a/SantaClaraCityCams/Info.plist b/SantaClaraCityCams/Info.plist new file mode 100644 index 0000000..e1fe4cf --- /dev/null +++ b/SantaClaraCityCams/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/SantaClaraCityCams/Models/SCCCameraLocation.h b/SantaClaraCityCams/Models/SCCCameraLocation.h new file mode 100644 index 0000000..94e2d7d --- /dev/null +++ b/SantaClaraCityCams/Models/SCCCameraLocation.h @@ -0,0 +1,41 @@ +// +// SCCCameraLocation.h +// SantaClaraCityCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +@import CoreLocation; +@import MapKit; + +typedef uint32_t SCCFrameIndex; + +NS_ASSUME_NONNULL_BEGIN + +@interface SCCCameraLocation : NSObject + +@property (strong, class, nonatomic, readonly) NSArray *knownLocations; + +@property (strong, nonatomic, readonly) NSString *localizedName; +@property (strong, nonatomic, readonly) NSString *key; +@property (strong, nonatomic, readonly) CLLocation *location; +@property (nonatomic, readonly) CLLocationDirection heading; + +@property (nonatomic, readonly) double framesPerSecond; +@property (nonatomic, readonly) SCCFrameIndex maxFrame; + +- (instancetype)initWithLocalizedName:(NSString *)name key:(NSString *)key location:(CLLocation *)location heading:(CLLocationDirection)heading; ++ (instancetype)cameraWithName:(NSString *)name key:(NSString *)key location:(CLLocation *)location heading:(CLLocationDirection)heading; + +- (void)currentFrameWithCallback:(void(^)(SCCFrameIndex index, NSError *err))callback; + +- (NSURL *)urlForFrame:(SCCFrameIndex)frame; + +// MARK: - Utility methods + ++ (NSDate *)lastModifiedDateFromServerResponse:(NSURLResponse *)response error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SantaClaraCityCams/Models/SCCCameraLocation.m b/SantaClaraCityCams/Models/SCCCameraLocation.m new file mode 100644 index 0000000..2cf02c6 --- /dev/null +++ b/SantaClaraCityCams/Models/SCCCameraLocation.m @@ -0,0 +1,187 @@ +// +// SCCCameraLocation.m +// SantaClaraCityCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +#import "SCCCameraLocation.h" + +@implementation SCCCameraLocation + +// MARK: - Class property synthesis + ++ (NSArray *)knownLocations { + static dispatch_once_t onceToken; + static NSArray *ret; + dispatch_once(&onceToken, ^{ + ret = @[ + [self cameraWithName:@"San Tomas Aquino Creek Trail @ Mission College" key:@"MissionCreekTrail" + location:[[CLLocation alloc] initWithLatitude:37.389137 longitude:-121.968255] heading:289], + [self cameraWithName:@"Bowers Ave @ SB101 Ramps" key:@"Bowers101" + location:[[CLLocation alloc] initWithLatitude:37.384560 longitude:-121.977493] heading:41], + [self cameraWithName:@"Great America @ 101 Off-Ramp NB" key:@"GA101" + location:[[CLLocation alloc] initWithLatitude:37.386760 longitude:-121.976634] heading:36], + [self cameraWithName:@"Great America @ Bunker Hill" key:@"GAParkwayBunkerHill" + location:[[CLLocation alloc] initWithLatitude:37.405447 longitude:-121.977944] heading:19], + [self cameraWithName:@"Great America @ Mission College" key:@"GAMissionCollege" + location:[[CLLocation alloc] initWithLatitude:37.392569 longitude:-121.976966] heading:117], + [self cameraWithName:@"Great America @ Patrick Henry" key:@"GAPatrickHenry" + location:[[CLLocation alloc] initWithLatitude:37.395812 longitude:-121.978110] heading:24], + [self cameraWithName:@"Great America @ Old Glory" key:@"GAOldGlory" + location:[[CLLocation alloc] initWithLatitude:37.399989 longitude:-121.978148] heading:146], + [self cameraWithName:@"Great America @ Tasman" key:@"GATasman" + location:[[CLLocation alloc] initWithLatitude:37.403059 longitude:-121.978006] heading:65], + [self cameraWithName:@"Great America @ Old Mtn View Alviso" key:@"GAOldMtnViewAlviso" + location:[[CLLocation alloc] initWithLatitude:37.410354 longitude:-121.977903] heading:162], + [self cameraWithName:@"Great America @ Great America Pkwy" key:@"GAGAParkway" + location:[[CLLocation alloc] initWithLatitude:37.414011 longitude:-121.977409] heading:337], + [self cameraWithName:@"Mission College @ Agnew" key:@"MissionCollegeAgnew" + location:[[CLLocation alloc] initWithLatitude:37.388898 longitude:-121.969322] heading:305], + [self cameraWithName:@"Mission College @ Mission College Circle" key:@"GAMissionCollegeCircle" + location:[[CLLocation alloc] initWithLatitude:37.391697 longitude:-121.978794] heading:308], + [self cameraWithName:@"Tasman @ Convention Center" key:@"TasmanConventionCenter" + location:[[CLLocation alloc] initWithLatitude:37.403637 longitude:-121.975551] heading:120], + [self cameraWithName:@"Tasman @ Old Ironsides" key:@"TasmanOldIronsides" + location:[[CLLocation alloc] initWithLatitude:37.403143 longitude:-121.979995] heading:309], + [self cameraWithName:@"Tasman @ Calle De Sol" key:@"TasmanCalleDeSol" + location:[[CLLocation alloc] initWithLatitude:37.407359 longitude:-121.963758] heading:282], + [self cameraWithName:@"Tasman @ Lick Mill" key:@"TasmanLickMill" + location:[[CLLocation alloc] initWithLatitude:37.408365 longitude:-121.961359] heading:251], + ]; + }); + return ret; +} + +// MARK: - Initializers + ++ (instancetype)cameraWithName:(NSString *)name key:(NSString *)key location:(CLLocation *)location heading:(CLLocationDirection)heading { + return [[self alloc] initWithLocalizedName:name key:key location:location heading:heading]; +} + +- (instancetype)initWithLocalizedName:(NSString *)name key:(NSString *)key location:(CLLocation *)location heading:(CLLocationDirection)heading { + if (self = [self init]) { + _localizedName = name; + _key = key; + _location = location; + _heading = heading; + } + return self; +} + +// MARK: - Readonly property synthesis + +- (double)framesPerSecond { + return 8/9.0; +} + +- (SCCFrameIndex)maxFrame { + return 0xff; +} + +// MARK: - Private methods + +- (NSURL *)_baseURL { + static dispatch_once_t onceToken; + static NSURL *ret; + dispatch_once(&onceToken, ^{ + ret = [NSURL URLWithString:@"https://trafficcam.santaclaraca.gov/Feeds"]; + }); + return ret; +} + ++ (NSError *)_errorWithSubdomain:(NSString *)subdomain description:(NSString *)desc { + NSString *domain = [@"null.leptos.SantaClaraCityCams" stringByAppendingPathExtension:subdomain]; + return [NSError errorWithDomain:domain code:1 userInfo:@{ + NSLocalizedDescriptionKey : desc + }]; +} + +// MARK: - Public methods + ++ (NSDate *)lastModifiedDateFromServerResponse:(NSURLResponse *)response error:(NSError **)error { + if (![response isKindOfClass:[NSHTTPURLResponse class]]) { + if (error) { + *error = [self _errorWithSubdomain:@"serverResponse.dateParse" description:@"The server response was in an unexpected format"]; + } + return nil; + } + + NSHTTPURLResponse *castedResponse = (NSHTTPURLResponse *)response; + NSString *fileDate = castedResponse.allHeaderFields[@"Last-Modified"]; + if (!fileDate) { + if (error) { + *error = [self _errorWithSubdomain:@"serverResponse.dateParse" description:@"The server did not provide a file modification date"]; + } + return nil; + } + + static NSDateFormatter *serverDateFormatter; + static dispatch_once_t serverDateFormatterCreateOnceToken; + dispatch_once(&serverDateFormatterCreateOnceToken, ^{ + serverDateFormatter = [NSDateFormatter new]; + serverDateFormatter.dateFormat = @"E, d MMM yyyy HH:mm:ss zzz"; + }); + + NSDate *firstFrameDate = [serverDateFormatter dateFromString:fileDate]; + if (!firstFrameDate) { + if (error) { + *error = [self _errorWithSubdomain:@"serverResponse.dateParse" description:@"The server date format could not be parsed"]; + } + return nil; + } + return firstFrameDate; +} + +- (NSURL *)urlForFrame:(SCCFrameIndex)frame { + NSURL *feedDir = [[self _baseURL] URLByAppendingPathComponent:self.key isDirectory:YES]; + NSString *imageName = [NSString stringWithFormat:@"snap_%03u_c1.jpg", frame]; + return [feedDir URLByAppendingPathComponent:imageName isDirectory:NO]; +} + +- (void)currentFrameWithCallback:(void(^)(SCCFrameIndex index, NSError *err))callback { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[self urlForFrame:1]]; + request.HTTPMethod = @"HEAD"; + [[NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + return callback(0, error); + } + + NSDate *firstFrameDate = [self.class lastModifiedDateFromServerResponse:response error:&error]; + if (error) { + return callback(0, error); + } + + NSTimeInterval diff = -firstFrameDate.timeIntervalSinceNow; // flip the sign bit + if (signbit(diff)) { // check if the sign bit is still set + return callback(0, [self.class _errorWithSubdomain:@"serverResponse" description:@"Parsed date returned by server is in the future"]); + } + double frameOffset = floor(diff * self.framesPerSecond); + frameOffset -= 3; // lag patch + SCCFrameIndex realFrame = self.maxFrame; + realFrame = frameOffset - realFrame*floor(frameOffset/realFrame); + if (realFrame == 0) { + realFrame = self.maxFrame; + } + callback(realFrame, nil); + }] resume]; +} + +// MARK: - MKAnnotation + +- (CLLocationCoordinate2D)coordinate { + return self.location.coordinate; +} + +- (NSString *)title { + return self.localizedName; +} + +// MARK: - Description + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p> key: %@", [self class], self, self.key]; +} + +@end diff --git a/SantaClaraCityCams/SantaClaraCityCams.h b/SantaClaraCityCams/SantaClaraCityCams.h new file mode 100644 index 0000000..2519579 --- /dev/null +++ b/SantaClaraCityCams/SantaClaraCityCams.h @@ -0,0 +1,9 @@ +// +// SantaClaraCityCams.h +// SantaClaraCityCams +// +// Created by Leptos on 3/23/19. +// Copyright © 2019 Leptos. All rights reserved. +// + +#import diff --git a/Screenshots/gabh_in.png b/Screenshots/gabh_in.png new file mode 100644 index 0000000..f76993b Binary files /dev/null and b/Screenshots/gabh_in.png differ diff --git a/Screenshots/map_spread.png b/Screenshots/map_spread.png new file mode 100644 index 0000000..b31a19c Binary files /dev/null and b/Screenshots/map_spread.png differ diff --git a/Screenshots/stact_in.png b/Screenshots/stact_in.png new file mode 100644 index 0000000..2f2cd6d Binary files /dev/null and b/Screenshots/stact_in.png differ diff --git a/Screenshots/stact_out.png b/Screenshots/stact_out.png new file mode 100644 index 0000000..a513201 Binary files /dev/null and b/Screenshots/stact_out.png differ diff --git a/Screenshots/table_view.png b/Screenshots/table_view.png new file mode 100644 index 0000000..088c3d8 Binary files /dev/null and b/Screenshots/table_view.png differ