From 1d03f1ba4b085693a52811d607dd3acb2fbb17d4 Mon Sep 17 00:00:00 2001 From: Aleksey Belousov Date: Thu, 19 Mar 2020 20:18:15 +0300 Subject: [PATCH] [iOS] add chart framefork for elevation profile --- iphone/Chart/Chart.xcodeproj/project.pbxproj | 403 ++++++++++++++++++ iphone/Chart/Chart/Chart.h | 7 + iphone/Chart/Chart/ChartData/ChartData.swift | 29 ++ .../ChartData/ChartPresentationData.swift | 228 ++++++++++ iphone/Chart/Chart/Info.plist | 22 + .../Chart/Views/ChartInfo/ChartInfoView.swift | 160 +++++++ .../Views/ChartInfo/ChartMyPositionView.swift | 83 ++++ .../Views/ChartInfo/ChartPointInfoView.swift | 118 +++++ .../ChartPointIntersectionsView.swift | 84 ++++ iphone/Chart/Chart/Views/ChartLineView.swift | 112 +++++ .../Chart/Chart/Views/ChartPreviewView.swift | 246 +++++++++++ iphone/Chart/Chart/Views/ChartView.swift | 297 +++++++++++++ iphone/Chart/Chart/Views/ChartXAxisView.swift | 110 +++++ iphone/Chart/Chart/Views/ChartYAxisView.swift | 227 ++++++++++ iphone/Maps/Maps.xcodeproj/project.pbxproj | 4 + .../omim.xcworkspace/contents.xcworkspacedata | 3 + 16 files changed, 2133 insertions(+) create mode 100644 iphone/Chart/Chart.xcodeproj/project.pbxproj create mode 100644 iphone/Chart/Chart/Chart.h create mode 100644 iphone/Chart/Chart/ChartData/ChartData.swift create mode 100644 iphone/Chart/Chart/ChartData/ChartPresentationData.swift create mode 100644 iphone/Chart/Chart/Info.plist create mode 100644 iphone/Chart/Chart/Views/ChartInfo/ChartInfoView.swift create mode 100644 iphone/Chart/Chart/Views/ChartInfo/ChartMyPositionView.swift create mode 100644 iphone/Chart/Chart/Views/ChartInfo/ChartPointInfoView.swift create mode 100644 iphone/Chart/Chart/Views/ChartInfo/ChartPointIntersectionsView.swift create mode 100644 iphone/Chart/Chart/Views/ChartLineView.swift create mode 100644 iphone/Chart/Chart/Views/ChartPreviewView.swift create mode 100644 iphone/Chart/Chart/Views/ChartView.swift create mode 100644 iphone/Chart/Chart/Views/ChartXAxisView.swift create mode 100644 iphone/Chart/Chart/Views/ChartYAxisView.swift diff --git a/iphone/Chart/Chart.xcodeproj/project.pbxproj b/iphone/Chart/Chart.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..67a435ded3 --- /dev/null +++ b/iphone/Chart/Chart.xcodeproj/project.pbxproj @@ -0,0 +1,403 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 47375D9B2420D4DB00FFCC49 /* Chart.h in Headers */ = {isa = PBXBuildFile; fileRef = 47375D992420D4DB00FFCC49 /* Chart.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 47375E3C2420E94E00FFCC49 /* ChartPresentationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E392420E94C00FFCC49 /* ChartPresentationData.swift */; }; + 47375E3E2420E94E00FFCC49 /* ChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E3B2420E94E00FFCC49 /* ChartData.swift */; }; + 47375E4B2420E97100FFCC49 /* ChartXAxisView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E3F2420E96C00FFCC49 /* ChartXAxisView.swift */; }; + 47375E4C2420E97100FFCC49 /* ChartLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E402420E96D00FFCC49 /* ChartLineView.swift */; }; + 47375E4D2420E97100FFCC49 /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E412420E96D00FFCC49 /* ChartView.swift */; }; + 47375E4E2420E97100FFCC49 /* ChartInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E432420E96F00FFCC49 /* ChartInfoView.swift */; }; + 47375E4F2420E97100FFCC49 /* ChartPointInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E442420E96F00FFCC49 /* ChartPointInfoView.swift */; }; + 47375E502420E97100FFCC49 /* ChartPointIntersectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E452420E96F00FFCC49 /* ChartPointIntersectionsView.swift */; }; + 47375E512420E97100FFCC49 /* ChartMyPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E462420E96F00FFCC49 /* ChartMyPositionView.swift */; }; + 47375E522420E97100FFCC49 /* ChartYAxisView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E472420E96F00FFCC49 /* ChartYAxisView.swift */; }; + 47375E542420E97100FFCC49 /* ChartPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47375E492420E97100FFCC49 /* ChartPreviewView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 47375D962420D4DB00FFCC49 /* Chart.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Chart.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 47375D992420D4DB00FFCC49 /* Chart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Chart.h; sourceTree = ""; }; + 47375D9A2420D4DB00FFCC49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 47375E392420E94C00FFCC49 /* ChartPresentationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPresentationData.swift; sourceTree = ""; }; + 47375E3B2420E94E00FFCC49 /* ChartData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartData.swift; sourceTree = ""; }; + 47375E3F2420E96C00FFCC49 /* ChartXAxisView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartXAxisView.swift; sourceTree = ""; }; + 47375E402420E96D00FFCC49 /* ChartLineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartLineView.swift; sourceTree = ""; }; + 47375E412420E96D00FFCC49 /* ChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; + 47375E432420E96F00FFCC49 /* ChartInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartInfoView.swift; sourceTree = ""; }; + 47375E442420E96F00FFCC49 /* ChartPointInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointInfoView.swift; sourceTree = ""; }; + 47375E452420E96F00FFCC49 /* ChartPointIntersectionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointIntersectionsView.swift; sourceTree = ""; }; + 47375E462420E96F00FFCC49 /* ChartMyPositionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartMyPositionView.swift; sourceTree = ""; }; + 47375E472420E96F00FFCC49 /* ChartYAxisView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartYAxisView.swift; sourceTree = ""; }; + 47375E492420E97100FFCC49 /* ChartPreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPreviewView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 47375D932420D4DB00FFCC49 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 47375D8C2420D4DB00FFCC49 = { + isa = PBXGroup; + children = ( + 47375D982420D4DB00FFCC49 /* Chart */, + 47375D972420D4DB00FFCC49 /* Products */, + ); + sourceTree = ""; + }; + 47375D972420D4DB00FFCC49 /* Products */ = { + isa = PBXGroup; + children = ( + 47375D962420D4DB00FFCC49 /* Chart.framework */, + ); + name = Products; + sourceTree = ""; + }; + 47375D982420D4DB00FFCC49 /* Chart */ = { + isa = PBXGroup; + children = ( + 47375DC22420D60200FFCC49 /* ChartData */, + 47375DC72420D60300FFCC49 /* Views */, + 47375D992420D4DB00FFCC49 /* Chart.h */, + 47375D9A2420D4DB00FFCC49 /* Info.plist */, + ); + path = Chart; + sourceTree = ""; + }; + 47375DC22420D60200FFCC49 /* ChartData */ = { + isa = PBXGroup; + children = ( + 47375E3B2420E94E00FFCC49 /* ChartData.swift */, + 47375E392420E94C00FFCC49 /* ChartPresentationData.swift */, + ); + path = ChartData; + sourceTree = ""; + }; + 47375DC72420D60300FFCC49 /* Views */ = { + isa = PBXGroup; + children = ( + 47375E422420E96F00FFCC49 /* ChartInfo */, + 47375E412420E96D00FFCC49 /* ChartView.swift */, + 47375E402420E96D00FFCC49 /* ChartLineView.swift */, + 47375E492420E97100FFCC49 /* ChartPreviewView.swift */, + 47375E3F2420E96C00FFCC49 /* ChartXAxisView.swift */, + 47375E472420E96F00FFCC49 /* ChartYAxisView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 47375E422420E96F00FFCC49 /* ChartInfo */ = { + isa = PBXGroup; + children = ( + 47375E432420E96F00FFCC49 /* ChartInfoView.swift */, + 47375E442420E96F00FFCC49 /* ChartPointInfoView.swift */, + 47375E452420E96F00FFCC49 /* ChartPointIntersectionsView.swift */, + 47375E462420E96F00FFCC49 /* ChartMyPositionView.swift */, + ); + path = ChartInfo; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 47375D912420D4DB00FFCC49 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 47375D9B2420D4DB00FFCC49 /* Chart.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 47375D952420D4DB00FFCC49 /* Chart */ = { + isa = PBXNativeTarget; + buildConfigurationList = 47375D9E2420D4DB00FFCC49 /* Build configuration list for PBXNativeTarget "Chart" */; + buildPhases = ( + 47375D912420D4DB00FFCC49 /* Headers */, + 47375D922420D4DB00FFCC49 /* Sources */, + 47375D932420D4DB00FFCC49 /* Frameworks */, + 47375D942420D4DB00FFCC49 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Chart; + productName = Chart; + productReference = 47375D962420D4DB00FFCC49 /* Chart.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 47375D8D2420D4DB00FFCC49 /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = maps.me; + TargetAttributes = { + 47375D952420D4DB00FFCC49 = { + CreatedOnToolsVersion = 11.3; + }; + }; + }; + buildConfigurationList = 47375D902420D4DB00FFCC49 /* Build configuration list for PBXProject "Chart" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 47375D8C2420D4DB00FFCC49; + productRefGroup = 47375D972420D4DB00FFCC49 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 47375D952420D4DB00FFCC49 /* Chart */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 47375D942420D4DB00FFCC49 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 47375D922420D4DB00FFCC49 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 47375E512420E97100FFCC49 /* ChartMyPositionView.swift in Sources */, + 47375E3E2420E94E00FFCC49 /* ChartData.swift in Sources */, + 47375E4C2420E97100FFCC49 /* ChartLineView.swift in Sources */, + 47375E502420E97100FFCC49 /* ChartPointIntersectionsView.swift in Sources */, + 47375E522420E97100FFCC49 /* ChartYAxisView.swift in Sources */, + 47375E4B2420E97100FFCC49 /* ChartXAxisView.swift in Sources */, + 47375E542420E97100FFCC49 /* ChartPreviewView.swift in Sources */, + 47375E3C2420E94E00FFCC49 /* ChartPresentationData.swift in Sources */, + 47375E4D2420E97100FFCC49 /* ChartView.swift in Sources */, + 47375E4F2420E97100FFCC49 /* ChartPointInfoView.swift in Sources */, + 47375E4E2420E97100FFCC49 /* ChartInfoView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 47375D9C2420D4DB00FFCC49 /* 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 47375D9D2420D4DB00FFCC49 /* 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 47375D9F2420D4DB00FFCC49 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N9X2A789QT; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Chart/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.mapswithme.Chart; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 47375DA02420D4DB00FFCC49 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N9X2A789QT; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Chart/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.mapswithme.Chart; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 47375D902420D4DB00FFCC49 /* Build configuration list for PBXProject "Chart" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 47375D9C2420D4DB00FFCC49 /* Debug */, + 47375D9D2420D4DB00FFCC49 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 47375D9E2420D4DB00FFCC49 /* Build configuration list for PBXNativeTarget "Chart" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 47375D9F2420D4DB00FFCC49 /* Debug */, + 47375DA02420D4DB00FFCC49 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 47375D8D2420D4DB00FFCC49 /* Project object */; +} diff --git a/iphone/Chart/Chart/Chart.h b/iphone/Chart/Chart/Chart.h new file mode 100644 index 0000000000..9dd126c804 --- /dev/null +++ b/iphone/Chart/Chart/Chart.h @@ -0,0 +1,7 @@ +#import + +//! Project version number for Chart. +FOUNDATION_EXPORT double ChartVersionNumber; + +//! Project version string for Chart. +FOUNDATION_EXPORT const unsigned char ChartVersionString[]; diff --git a/iphone/Chart/Chart/ChartData/ChartData.swift b/iphone/Chart/Chart/ChartData/ChartData.swift new file mode 100644 index 0000000000..d810ca57e3 --- /dev/null +++ b/iphone/Chart/Chart/ChartData/ChartData.swift @@ -0,0 +1,29 @@ +import UIKit + +public enum ChartType { + case regular + case percentage +} + +public enum ChartLineType: String { + case line = "line" + case lineArea = "lineArea" +} + +public protocol IFormatter { + func distanceString(from value: Double) -> String + func altitudeString(from value: Double) -> String +} + +public protocol IChartData { + var xAxisValues: [Double] { get } + var lines: [IChartLine] { get } + var type: ChartType { get } +} + +public protocol IChartLine { + var values: [Int] { get } + var name: String { get } + var color: UIColor { get } + var type: ChartLineType { get } +} diff --git a/iphone/Chart/Chart/ChartData/ChartPresentationData.swift b/iphone/Chart/Chart/ChartData/ChartPresentationData.swift new file mode 100644 index 0000000000..e885008153 --- /dev/null +++ b/iphone/Chart/Chart/ChartData/ChartPresentationData.swift @@ -0,0 +1,228 @@ +import UIKit + +public class ChartPresentationData { + private let chartData: IChartData + private var presentationLines: [ChartPresentationLine] + private let pathBuilder = ChartPathBuilder() + private let useFilter: Bool + let formatter: IFormatter + + public init(_ chartData: IChartData, formatter: IFormatter, useFilter: Bool = false) { + self.chartData = chartData + self.useFilter = useFilter + self.formatter = formatter + presentationLines = chartData.lines.map { ChartPresentationLine($0, useFilter: useFilter) } + labels = chartData.xAxisValues.map { formatter.distanceString(from: $0) } + recalcBounds() + } + + var linesCount: Int { chartData.lines.count } + var pointsCount: Int { chartData.xAxisValues.count } + var xAxisValues: [Double] { chartData.xAxisValues } + var type: ChartType { chartData.type } + var labels: [String] + var lower = CGFloat(Int.max) + var upper = CGFloat(Int.min) + + func labelAt(_ point: CGFloat) -> String { + let p1 = Int(floor(point)) + let p2 = Int(ceil(point)) + let v1 = chartData.xAxisValues[p1] + let v2 = chartData.xAxisValues[p2] + let value = v1 + (v2 - v1) * Double(point.truncatingRemainder(dividingBy: 1)) + return formatter.distanceString(from: value) + } + + func lineAt(_ index: Int) -> ChartPresentationLine { + return presentationLines[index] + } + + private func recalcBounds() { + presentationLines.forEach { $0.aggregatedValues = [] } + pathBuilder.build(presentationLines, type: type) + + var l = CGFloat(Int.max) + var u = CGFloat(Int.min) + presentationLines.forEach { + l = min($0.minY, l) + u = max($0.maxY, u) + } + lower = l + upper = u + } +} + +class ChartPresentationLine { + private let chartLine: IChartLine + private let useFilter: Bool + + var aggregatedValues: [CGFloat] = [] + var minY: CGFloat = CGFloat(Int.max) + var maxY: CGFloat = CGFloat(Int.min) + var path = UIBezierPath() + var previewPath = UIBezierPath() + + var values: [CGFloat] + var originalValues: [Int] { return chartLine.values } + var color: UIColor { return chartLine.color } + var name: String { return chartLine.name } + var type: ChartLineType { return chartLine.type } + + init(_ chartLine: IChartLine, useFilter: Bool = false) { + self.chartLine = chartLine + self.useFilter = useFilter + if useFilter { + var filter = LowPassFilter(value: CGFloat(chartLine.values[0]), filterFactor: 0.8) + values = chartLine.values.map { + filter.update(newValue: CGFloat($0)) + return filter.value + } + } else { + values = chartLine.values.map { CGFloat($0) } + } + } +} + +protocol IChartPathBuilder { + func build(_ lines: [ChartPresentationLine]) + func makeLinePath(line: ChartPresentationLine) -> UIBezierPath + func makePercentLinePath(line: ChartPresentationLine, bottomLine: ChartPresentationLine?) -> UIBezierPath +} + +extension IChartPathBuilder { + func makeLinePreviewPath(line: ChartPresentationLine) -> UIBezierPath { + var filter = LowPassFilter(value: 0, filterFactor: 0.3) + let path = UIBezierPath() + let step = 5 + let values = line.values.enumerated().compactMap { $0 % step == 0 ? $1 : nil } + for i in 0.. UIBezierPath { + let path = UIBezierPath() + let values = line.values + for i in 0.. UIBezierPath { + let path = UIBezierPath() + path.move(to: CGPoint(x: 0, y: 0)) + var filter = LowPassFilter(value: 0, filterFactor: 0.3) + + let step = 5 + let aggregatedValues = line.aggregatedValues.enumerated().compactMap { $0 % step == 0 ? $1 : nil } + for i in 0.. UIBezierPath { + let path = UIBezierPath() + path.move(to: CGPoint(x: 0, y: 0)) + + let aggregatedValues = line.aggregatedValues + for i in 0.. + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/iphone/Chart/Chart/Views/ChartInfo/ChartInfoView.swift b/iphone/Chart/Chart/Views/ChartInfo/ChartInfoView.swift new file mode 100644 index 0000000000..1e6dcc0006 --- /dev/null +++ b/iphone/Chart/Chart/Views/ChartInfo/ChartInfoView.swift @@ -0,0 +1,160 @@ +import UIKit + +struct ChartLineInfo { + let name: String + let color: UIColor + let point: CGPoint + let formattedValue: String +} + +protocol ChartInfoViewDelegate: AnyObject { + func chartInfoView(_ view: ChartInfoView, infoAtPointX pointX: CGFloat) -> (String, [ChartLineInfo])? + func chartInfoView(_ view: ChartInfoView, didCaptureInfoView captured: Bool) +} + +class ChartInfoView: UIView { + weak var delegate: ChartInfoViewDelegate? + + private let pointInfoView = ChartPointInfoView() + private let pointsView = ChartPointIntersectionsView(frame: CGRect(x: 0, y: 0, width: 2, height: 0)) + private let myPositionView = ChartMyPositionView(frame: CGRect(x: 50, y: 0, width: 2, height: 0)) + private var lineInfo: ChartLineInfo? + + fileprivate var captured = false { + didSet { + delegate?.chartInfoView(self, didCaptureInfoView: captured) + } + } + + var infoX: CGFloat = 0 { + didSet { + if bounds.width > 0 { + let x = bounds.width * infoX + update(x) + updateViews(point: lineInfo!.point) + } + } + } + + var bgColor: UIColor = UIColor.white + + var textColor: UIColor = UIColor.black { + didSet { + pointInfoView.textColor = textColor + } + } + + var panGR: UIPanGestureRecognizer! + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(myPositionView) + myPositionView.isHidden = true + addSubview(pointsView) + addSubview(pointInfoView) + isExclusiveTouch = true + panGR = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))) + panGR.delegate = self + addGestureRecognizer(panGR) + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + bounds.insetBy(dx: -22, dy: 0).contains(point) + } + + func update(_ x: CGFloat? = nil) { + let x = x ?? pointsView.center.x + guard let delegate = delegate, + let (label, intersectionPoints) = delegate.chartInfoView(self, infoAtPointX: x) else { return } + lineInfo = intersectionPoints[0] + self.pointsView.updatePoints(intersectionPoints) + pointInfoView.update(x: x, label: label, points: intersectionPoints) + updateViews(point: lineInfo!.point) + let y = max(pointInfoView.frame.height / 2 + 5, + min(bounds.height - pointInfoView.frame.height / 2 - 5, bounds.height - lineInfo!.point.y)); + pointInfoView.center = CGPoint(x: pointInfoView.center.x, y: y) + let arrowPoint = convert(CGPoint(x: 0, y: bounds.height - lineInfo!.point.y), to: pointInfoView) + pointInfoView.arrowY = arrowPoint.y + } + + @objc func onPan(_ sender: UIPanGestureRecognizer) { + let x = sender.location(in: self).x + switch sender.state { + case .possible: + break + case .began: + guard let lineInfo = lineInfo else { return } + captured = abs(x - lineInfo.point.x) < 22 + case .changed: + if captured { + if x < bounds.minX || x > bounds.maxX { + return + } + update(x) + updateViews(point: lineInfo!.point) + } + case .ended, .cancelled, .failed: + captured = false + @unknown default: + fatalError() + } + } + + func updateViews(point: CGPoint) { + pointsView.alpha = 1 + pointsView.center = CGPoint(x: point.x, y: bounds.midY) + + let s = pointInfoView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + pointInfoView.frame.size = s + let orientationChangeX = pointInfoView.alignment == .left ? s.width + 40 : bounds.width - s.width - 40 + if point.x > orientationChangeX { + pointInfoView.alignment = .left + pointInfoView.center = CGPoint(x: point.x - s.width / 2 - 20, y: pointInfoView.center.y) + } else { + pointInfoView.alignment = .right + pointInfoView.center = CGPoint(x: point.x + s.width / 2 + 20, y: pointInfoView.center.y) + } + var f = pointInfoView.frame + if f.minX < 0 { + f.origin.x = 0 + pointInfoView.frame = f + } else if f.minX + f.width > bounds.width { + f.origin.x = bounds.width - f.width + pointInfoView.frame = f + } + } + + override func layoutSubviews() { + super.layoutSubviews() + var pf = pointsView.frame + pf.origin.y = bounds.minY + pf.size.height = bounds.height + pointsView.frame = pf + + var mf = myPositionView.frame + mf.origin.y = bounds.minY + mf.size.height = bounds.height + myPositionView.frame = mf + + if lineInfo == nil, bounds.width > 0 { + let x = bounds.width * infoX + guard let (date, intersectionPoints) = delegate?.chartInfoView(self, infoAtPointX: x) else { return } + lineInfo = intersectionPoints[0] + pointsView.setPoints(intersectionPoints) + pointInfoView.set(x: x, label: date, points: intersectionPoints) + updateViews(point: lineInfo!.point) + } + } +} + +extension ChartInfoView: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return !captured + } +} diff --git a/iphone/Chart/Chart/Views/ChartInfo/ChartMyPositionView.swift b/iphone/Chart/Chart/Views/ChartInfo/ChartMyPositionView.swift new file mode 100644 index 0000000000..c1b944e6c7 --- /dev/null +++ b/iphone/Chart/Chart/Views/ChartInfo/ChartMyPositionView.swift @@ -0,0 +1,83 @@ +import UIKit + +class ChartMyPositionView: UIView { + override class var layerClass: AnyClass { CAShapeLayer.self } + var shapeLayer: CAShapeLayer { layer as! CAShapeLayer } + + fileprivate let pinView = MyPositionPinView(frame: CGRect(x: 0, y: 0, width: 12, height: 16)) + + override init(frame: CGRect) { + super.init(frame: frame) + + shapeLayer.lineDashPattern = [3, 2] + shapeLayer.lineWidth = 2 + shapeLayer.strokeColor = UIColor(red: 0.142, green: 0.614, blue: 0.95, alpha: 0.3).cgColor + addSubview(pinView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + let path = UIBezierPath() + path.move(to: CGPoint(x: bounds.width / 2, y: 0)) + path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height)) + shapeLayer.path = path.cgPath + + pinView.center = CGPoint(x: bounds.midX, y: bounds.midY) + } +} + +fileprivate class MyPositionPinView: UIView { + override class var layerClass: AnyClass { CAShapeLayer.self } + var shapeLayer: CAShapeLayer { layer as! CAShapeLayer } + + var path: UIBezierPath = { + let p = UIBezierPath() + p.addArc(withCenter: CGPoint(x: 6, y: 6), + radius: 6, + startAngle: -CGFloat.pi / 2, + endAngle: atan(3.5 / 4.8733971724), + clockwise: true) + p.addLine(to: CGPoint(x: 6 + 0.75, y: 15.6614378)) + p.addArc(withCenter: CGPoint(x: 6, y: 15), + radius: 1, + startAngle: atan(0.6614378 / 0.75), + endAngle: CGFloat.pi - atan(0.6614378 / 0.75), + clockwise: true) + p.addLine(to: CGPoint(x: 6 - 4.8733971724, y: 9.5)) + p.addArc(withCenter: CGPoint(x: 6, y: 6), + radius: 6, + startAngle: CGFloat.pi - atan(3.5 / 4.8733971724), + endAngle: -CGFloat.pi / 2, + clockwise: true) + p.close() + + p.append(UIBezierPath(ovalIn: CGRect(x: 3, y: 3, width: 6, height: 6))) + return p + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + shapeLayer.lineWidth = 0 + shapeLayer.fillColor = UIColor(red: 0.142, green: 0.614, blue: 0.95, alpha: 0.5).cgColor + shapeLayer.fillRule = .evenOdd + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + let sx = bounds.width / path.bounds.width + let sy = bounds.height / path.bounds.height + let p = path.copy() as! UIBezierPath + p.apply(CGAffineTransform(scaleX: sx, y: sy)) + shapeLayer.path = p.cgPath + } +} diff --git a/iphone/Chart/Chart/Views/ChartInfo/ChartPointInfoView.swift b/iphone/Chart/Chart/Views/ChartInfo/ChartPointInfoView.swift new file mode 100644 index 0000000000..49b85d6823 --- /dev/null +++ b/iphone/Chart/Chart/Views/ChartInfo/ChartPointInfoView.swift @@ -0,0 +1,118 @@ +import UIKit + +class ChartPointInfoView: UIView { + enum Alignment { + case left + case right + } + + let captionLabel = UILabel() + let distanceLabel = UILabel() + let altitudeLabel = UILabel() + let stackView = UIStackView() + + let maskLayer = CAShapeLayer() + var maskPath: UIBezierPath? + + var arrowY: CGFloat? { + didSet { + setNeedsLayout() + } + } + + var alignment = Alignment.left { + didSet { + updateMask() + } + } + + var textColor: UIColor = .lightGray { + didSet { + captionLabel.textColor = textColor + distanceLabel.textColor = textColor + altitudeLabel.textColor = textColor + } + } + + let font = UIFont.systemFont(ofSize: 12, weight: .medium) + let lightFont = UIFont.systemFont(ofSize: 12) + + override init(frame: CGRect) { + super.init(frame: frame) + + layer.cornerRadius = 5 + backgroundColor = .clear + layer.shadowColor = UIColor(white: 0, alpha: 1).cgColor + layer.shadowOpacity = 0.25 + layer.shadowRadius = 2 + layer.shadowOffset = CGSize(width: 0, height: 2) + maskLayer.fillColor = UIColor.white.cgColor + layer.addSublayer(maskLayer) + + stackView.alignment = .leading + stackView.axis = .vertical + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 6), + stackView.topAnchor.constraint(equalTo: topAnchor, constant: 6), + stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -6), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -6) + ]) + + stackView.addArrangedSubview(captionLabel) + stackView.addArrangedSubview(distanceLabel) + stackView.addArrangedSubview(altitudeLabel) + stackView.setCustomSpacing(6, after: distanceLabel) + + captionLabel.text = "Distance:" + + captionLabel.font = lightFont + distanceLabel.font = lightFont + altitudeLabel.font = lightFont + + captionLabel.textColor = textColor + distanceLabel.textColor = textColor + altitudeLabel.textColor = textColor + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + + func set(x: CGFloat, label: String, points: [ChartLineInfo]) { + distanceLabel.text = label + altitudeLabel.text = "▲ \(points[0].formattedValue)" + } + + func update(x: CGFloat, label: String, points: [ChartLineInfo]) { + distanceLabel.text = label + altitudeLabel.text = "▲ \(points[0].formattedValue)" + layoutIfNeeded() + } + + override func layoutSubviews() { + super.layoutSubviews() + + let y = arrowY ?? bounds.midY + let path = UIBezierPath(roundedRect: bounds, cornerRadius: 3) + let trianglePath = UIBezierPath() + trianglePath.move(to: CGPoint(x: bounds.maxX, y: y - 3)) + trianglePath.addLine(to: CGPoint(x: bounds.maxX + 5, y: y)) + trianglePath.addLine(to: CGPoint(x: bounds.maxX, y: y + 3)) + trianglePath.close() + path.append(trianglePath) + maskPath = path + updateMask() + } + + private func updateMask() { + guard let path = maskPath?.copy() as? UIBezierPath else { return } + if alignment == .right { + path.apply(CGAffineTransform.identity.scaledBy(x: -1, y: 1).translatedBy(x: -bounds.width, y: 0)) + } + maskLayer.path = path.cgPath + layer.shadowPath = path.cgPath + } +} diff --git a/iphone/Chart/Chart/Views/ChartInfo/ChartPointIntersectionsView.swift b/iphone/Chart/Chart/Views/ChartInfo/ChartPointIntersectionsView.swift new file mode 100644 index 0000000000..ae9db6b642 --- /dev/null +++ b/iphone/Chart/Chart/Views/ChartInfo/ChartPointIntersectionsView.swift @@ -0,0 +1,84 @@ +import UIKit + +fileprivate class CircleView: UIView { + override class var layerClass: AnyClass { return CAShapeLayer.self } + + var color: UIColor? { + didSet { + shapeLayer.fillColor = color?.withAlphaComponent(0.5).cgColor + ringLayer.fillColor = UIColor.white.cgColor + centerLayer.fillColor = color?.cgColor + } + } + + var shapeLayer: CAShapeLayer { + return layer as! CAShapeLayer + } + + let ringLayer = CAShapeLayer() + let centerLayer = CAShapeLayer() + + override var frame: CGRect { + didSet { + let p = UIBezierPath(ovalIn: bounds) + shapeLayer.path = p.cgPath + ringLayer.frame = shapeLayer.bounds.insetBy(dx: shapeLayer.bounds.width / 6, dy: shapeLayer.bounds.height / 6) + ringLayer.path = UIBezierPath(ovalIn: ringLayer.bounds).cgPath + centerLayer.frame = shapeLayer.bounds.insetBy(dx: shapeLayer.bounds.width / 3, dy: shapeLayer.bounds.height / 3) + centerLayer.path = UIBezierPath(ovalIn: centerLayer.bounds).cgPath + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + shapeLayer.fillColor = color?.withAlphaComponent(0.5).cgColor + shapeLayer.lineWidth = 4 + shapeLayer.fillRule = .evenOdd + shapeLayer.addSublayer(ringLayer) + shapeLayer.addSublayer(centerLayer) + ringLayer.fillColor = UIColor.white.cgColor + centerLayer.fillColor = color?.cgColor + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } +} + +class ChartPointIntersectionsView: UIView { + fileprivate var intersectionViews: [CircleView] = [] + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = UIColor(red: 0.14, green: 0.61, blue: 0.95, alpha: 0.5) + transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1) + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + + func setPoints(_ points: [ChartLineInfo]) { + intersectionViews.forEach { $0.removeFromSuperview() } + intersectionViews.removeAll() + + for point in points { + let v = CircleView() + v.color = point.color + v.frame = CGRect(x: 0, y: 0, width: 24, height: 24) + v.center = CGPoint(x: bounds.midX, y: point.point.y) + intersectionViews.append(v) + addSubview(v) + } + } + + func updatePoints(_ points: [ChartLineInfo]) { + for i in 0.. UIBezierPath { + let cornersMask = UIBezierPath(roundedRect: bounds, cornerRadius: 5) + let rectMask = UIBezierPath(rect: bounds.insetBy(dx: 11, dy: 1)) + let result = UIBezierPath() + result.append(cornersMask) + result.append(rectMask) + result.usesEvenOddFillRule = true + return result + } +} + +class ChartPreviewView: UIView { + let previewContainerView = UIView() + let viewPortView = ViewPortView() + let leftBoundView = UIView() + let rightBoundView = UIView() + let tintView = TintView() + var previewViews: [ChartLineView] = [] + + var selectorColor: UIColor = UIColor.white { + didSet { + viewPortView.backgroundColor = selectorColor + } + } + + var selectorTintColor: UIColor = UIColor.clear { + didSet { + tintView.backgroundColor = selectorTintColor + } + } + + var minX = 0 + var maxX = 0 + weak var delegate: ChartPreviewViewDelegate? + + override var frame: CGRect { + didSet { + if chartData != nil { + updateViewPort() + } + } + } + + var chartData: ChartPresentationData! { + didSet { + previewViews.forEach { $0.removeFromSuperview() } + previewViews.removeAll() + for i in (0.. 0 && mx < count { + viewPortView.frame = viewPortView.frame.offsetBy(dx: p.x, dy: 0) + sender.setTranslation(CGPoint(x: 0, y: 0), in: viewPortView) + if x != minX { + minX = x + maxX = mx + delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX) + } + } else if minX > 0 && x <= 0 { + setX(min: 0, max: dx) + } else if maxX < count && mx >= count { + setX(min: count - dx, max: count) + } + } + + @objc func onLeftPan(_ sender: UIPanGestureRecognizer) { + if sender.state != .changed { return } + + let p = sender.translation(in: leftBoundView) + let count = chartData.labels.count - 1 + let x = Int((viewPortView.frame.minX + p.x) / bounds.width * CGFloat(count)) + + if x > 0 && x < maxX && maxX - x >= count / 10 { + var f = viewPortView.frame + f = CGRect(x: f.minX + p.x, y: f.minY, width: f.width - p.x, height: f.height) + viewPortView.frame = f + rightBoundView.frame = CGRect(x: viewPortView.bounds.width - 14, y: 0, width: 44, height: viewPortView.bounds.height) + sender.setTranslation(CGPoint(x: 0, y: 0), in: leftBoundView) + if x != minX { + minX = x + delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX) + } + } else if x <= 0 && minX > 0 { + setX(min: 0, max: maxX) + } + } + + @objc func onRightPan(_ sender: UIPanGestureRecognizer) { + if sender.state != .changed { return } + + let p = sender.translation(in: viewPortView) + let count = chartData.labels.count - 1 + let x = Int((viewPortView.frame.maxX + p.x) / bounds.width * CGFloat(count)) + + if x > minX && x < count && x - minX >= count / 10 { + var f = viewPortView.frame + f = CGRect(x: f.minX, y: f.minY, width: f.width + p.x, height: f.height) + viewPortView.frame = f + rightBoundView.frame = CGRect(x: viewPortView.bounds.width - 14, y: 0, width: 44, height: viewPortView.bounds.height) + sender.setTranslation(CGPoint(x: 0, y: 0), in: rightBoundView) + if x != maxX { + maxX = x + delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX) + } + } else if x >= count && maxX < count { + setX(min: minX, max: count) + } + } + + func setX(min: Int, max: Int) { + assert(min < max) + minX = min + maxX = max + updateViewPort() + delegate?.chartPreviewView(self, didChangeMinX: minX, maxX: maxX) + } + + func updateViewPort() { + let count = CGFloat(chartData.labels.count - 1) + viewPortView.frame = CGRect(x: CGFloat(minX) / count * bounds.width, + y: bounds.minY, + width: CGFloat(maxX - minX) / count * bounds.width, + height: bounds.height) + leftBoundView.frame = CGRect(x: -30, y: 0, width: 44, height: viewPortView.bounds.height) + rightBoundView.frame = CGRect(x: viewPortView.bounds.width - 14, y: 0, width: 44, height: viewPortView.bounds.height) + } +} diff --git a/iphone/Chart/Chart/Views/ChartView.swift b/iphone/Chart/Chart/Views/ChartView.swift new file mode 100644 index 0000000000..e8ed81ece3 --- /dev/null +++ b/iphone/Chart/Chart/Views/ChartView.swift @@ -0,0 +1,297 @@ +import UIKit + +enum ChartAnimation: TimeInterval { + case none = 0.0 + case animated = 0.3 + case interactive = 0.1 +} + +public class ChartView: UIView { + let chartsContainerView = UIView() + let chartPreviewView = ChartPreviewView() + let yAxisView = ChartYAxisView() + let xAxisView = ChartXAxisView() + let chartInfoView = ChartInfoView() + var lineViews: [ChartLineView] = [] + + private var panStartPoint = 0 + private var panGR: UIPanGestureRecognizer! + private var pinchStartLower = 0 + private var pinchStartUpper = 0 + private var pinchGR: UIPinchGestureRecognizer! + + public var previewSelectorColor: UIColor = UIColor.lightGray.withAlphaComponent(0.9) { + didSet { + chartPreviewView.selectorColor = previewSelectorColor + } + } + + public var previewTintColor: UIColor = UIColor.lightGray.withAlphaComponent(0.5) { + didSet { + chartPreviewView.selectorTintColor = previewTintColor + } + } + + public var headerTextColor: UIColor = UIColor.lightGray { + didSet { + chartInfoView.textColor = headerTextColor + } + } + + public var gridTextColor: UIColor = UIColor(white: 0, alpha: 0.2) { + didSet { + xAxisView.gridColor = gridTextColor + yAxisView.gridColor = gridTextColor + } + } + + public var gridLineColor: UIColor = UIColor(white: 0, alpha: 0.2) { + didSet { + yAxisView.gridLineColor = gridLineColor + } + } + + public var bgColor: UIColor = UIColor.white { + didSet { + chartInfoView.bgColor = bgColor + } + } + + weak var headerUpdateTimer: Timer? + + public var rasterize = false { + didSet { + lineViews.forEach { + $0.layer.shouldRasterize = rasterize + $0.layer.rasterizationScale = UIScreen.main.scale + } + } + } + + public var chartData: ChartPresentationData! { + didSet { + lineViews.forEach { $0.removeFromSuperview() } + lineViews.removeAll() + for i in (0.. Void + public var onSelectedPointChanged: OnSelectePointChangedClosure? + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + private func setup() { + xAxisView.gridColor = gridTextColor + yAxisView.gridColor = gridTextColor + yAxisView.gridLineColor = gridTextColor + chartInfoView.bgColor = bgColor + chartPreviewView.selectorTintColor = previewTintColor + chartPreviewView.selectorColor = previewSelectorColor + + panGR = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))) + chartsContainerView.addGestureRecognizer(panGR) + pinchGR = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(_:))) + chartsContainerView.addGestureRecognizer(pinchGR) + addSubview(chartsContainerView) + addSubview(chartPreviewView) + chartPreviewView.delegate = self + addSubview(xAxisView) + } + + public func setSelectedPoint(_ x: Double) { + let routeLength = chartData.xAxisValues.last! + chartInfoView.infoX = CGFloat(x / routeLength) + } + + override public func layoutSubviews() { + super.layoutSubviews() + + let previewFrame = CGRect(x: bounds.minX, y: bounds.maxY - 30, width: bounds.width, height: 30) + chartPreviewView.frame = previewFrame + + let xAxisFrame = CGRect(x: bounds.minX, y: bounds.maxY - previewFrame.height - 26, width: bounds.width, height: 26) + xAxisView.frame = xAxisFrame + + let chartsFrame = CGRect(x: bounds.minX, + y: bounds.minY, + width: bounds.width, + height: bounds.maxY - previewFrame.height - xAxisFrame.height) + chartsContainerView.frame = chartsFrame + } + + @objc func onPinch(_ sender: UIPinchGestureRecognizer) { + if sender.state == .began { + pinchStartLower = xAxisView.lowerBound + pinchStartUpper = xAxisView.upperBound + } + + if sender.state != .changed { + return + } + + let rangeLength = CGFloat(pinchStartUpper - pinchStartLower) + let dx = Int(round((rangeLength * sender.scale - rangeLength) / 2)) + let lower = max(pinchStartLower + dx, 0) + let upper = min(pinchStartUpper - dx, chartData.labels.count - 1) + + if upper - lower < chartData.labels.count / 10 { + return + } + + chartPreviewView.setX(min: lower, max: upper) + xAxisView.setBounds(lower: lower, upper: upper) + updateCharts(animationStyle: .none) + chartInfoView.update() + } + + @objc func onPan(_ sender: UIPanGestureRecognizer) { + let t = sender.translation(in: chartsContainerView) + if sender.state == .began { + panStartPoint = xAxisView.lowerBound + } + + if sender.state != .changed { + return + } + + let dx = Int(round(t.x / chartsContainerView.bounds.width * CGFloat(xAxisView.upperBound - xAxisView.lowerBound))) + let lower = panStartPoint - dx + let upper = lower + xAxisView.upperBound - xAxisView.lowerBound + if lower < 0 || upper > chartData.labels.count - 1 { + return + } + + chartPreviewView.setX(min: lower, max: upper) + xAxisView.setBounds(lower: lower, upper: upper) + updateCharts(animationStyle: .none) + chartInfoView.update() + } + + func updateCharts(animationStyle: ChartAnimation = .none) { + var lower = CGFloat(Int.max) + var upper = CGFloat(Int.min) + + for i in 0.. (String, [ChartLineInfo])? { + let p = convert(CGPoint(x: pointX, y: 0), from: view) + let x = (p.x / bounds.width) * CGFloat(xAxisView.upperBound - xAxisView.lowerBound) + CGFloat(xAxisView.lowerBound) + let x1 = Int(floor(x)) + let x2 = Int(ceil(x)) + guard x1 < chartData.labels.count && x >= 0 else { return nil } + let label = chartData.labelAt(x) + + var result: [ChartLineInfo] = [] + for i in 0.. 0 { + updateLabels() + } + } + } + + func makeLabel(text: String) -> UILabel { + let label = UILabel() + label.font = font + label.textColor = gridColor + label.text = text + label.frame = CGRect(x: 0, y: 0, width: 50, height: 15) + return label + } + + func setBounds(lower: Int, upper: Int, steps: [String]) { + lowerBound = lower + upperBound = upper + self.steps = steps + + labels.forEach { $0.removeFromSuperview() } + labels.removeAll() + + for i in 0.. 0 ? x / bounds.width : 0 + f.origin = CGPoint(x: x - f.width * adjust, y: 0) + l.frame = f.integral + } + } +} + +class ChartXAxisView: UIView { + var lowerBound = 0 + var upperBound = 0 + + var values: [String] = [] + + var gridColor: UIColor = UIColor(white: 0, alpha: 0.3) { + didSet { + labelsView?.gridColor = gridColor + } + } + + private var labelsView: ChartXAxisInnerView? + + func setBounds(lower: Int, upper: Int) { + lowerBound = lower + upperBound = upper + let step = CGFloat(upper - lower) / 5 + + var steps: [String] = [] + for i in 0..<5 { + let x = lower + Int(round(step * CGFloat(i))) + steps.append(values[x]) + } + steps.append(values[upper]) + + let lv = ChartXAxisInnerView() + lv.frame = bounds + lv.gridColor = gridColor + lv.autoresizingMask = [.flexibleWidth, .flexibleHeight] + addSubview(lv) + + if let labelsView = labelsView { + labelsView.removeFromSuperview() + } + + lv.setBounds(lower: lower, upper: upper, steps: steps) + labelsView = lv + } +} diff --git a/iphone/Chart/Chart/Views/ChartYAxisView.swift b/iphone/Chart/Chart/Views/ChartYAxisView.swift new file mode 100644 index 0000000000..b1a7dd4b6b --- /dev/null +++ b/iphone/Chart/Chart/Views/ChartYAxisView.swift @@ -0,0 +1,227 @@ +import UIKit + +enum ChartYAxisViewAlignment { + case left + case right +} + +fileprivate class ChartYAxisInnerView: UIView { + override class var layerClass: AnyClass { return CAShapeLayer.self } + + private static let font = UIFont.systemFont(ofSize: 12, weight: .regular) + var lowerBound: CGFloat = 0 + var upperBound: CGFloat = 0 + var steps: [CGFloat] = [] + let lowerLabel: UILabel + let upperLabel: UILabel + let lowerLabelBackground = UIView() + let upperLabelBackground = UIView() + var alignment: ChartYAxisViewAlignment = .left + var textColor: UIColor? + var gridColor: UIColor = UIColor(white: 0, alpha: 0.3) { + didSet { + if textColor == nil { + lowerLabel.textColor = gridColor + upperLabel.textColor = gridColor + } + } + } + + var gridLineColor: UIColor = UIColor.white { + didSet { + shapeLayer.strokeColor = gridLineColor.cgColor + } + } + + private var path: UIBezierPath? + + var shapeLayer: CAShapeLayer { + return layer as! CAShapeLayer + } + + override init(frame: CGRect) { + lowerLabel = ChartYAxisInnerView.makeLabel() + upperLabel = ChartYAxisInnerView.makeLabel() + + super.init(frame: frame) + + lowerLabel.translatesAutoresizingMaskIntoConstraints = false + upperLabel.translatesAutoresizingMaskIntoConstraints = false + lowerLabelBackground.translatesAutoresizingMaskIntoConstraints = false + upperLabelBackground.translatesAutoresizingMaskIntoConstraints = false + lowerLabelBackground.backgroundColor = UIColor(white: 1, alpha: 0.8) + upperLabelBackground.backgroundColor = UIColor(white: 1, alpha: 0.8) + + lowerLabelBackground.addSubview(lowerLabel) + upperLabelBackground.addSubview(upperLabel) + addSubview(lowerLabelBackground) + addSubview(upperLabelBackground) + + NSLayoutConstraint.activate([ + lowerLabel.leftAnchor.constraint(equalTo: lowerLabelBackground.leftAnchor, constant: 5), + lowerLabel.topAnchor.constraint(equalTo: lowerLabelBackground.topAnchor), + lowerLabel.rightAnchor.constraint(equalTo: lowerLabelBackground.rightAnchor, constant: -5), + lowerLabel.bottomAnchor.constraint(equalTo: lowerLabelBackground.bottomAnchor), + + upperLabel.leftAnchor.constraint(equalTo: upperLabelBackground.leftAnchor, constant: 5), + upperLabel.topAnchor.constraint(equalTo: upperLabelBackground.topAnchor), + upperLabel.rightAnchor.constraint(equalTo: upperLabelBackground.rightAnchor, constant: -5), + upperLabel.bottomAnchor.constraint(equalTo: upperLabelBackground.bottomAnchor), + + lowerLabelBackground.topAnchor.constraint(equalTo: topAnchor, constant: 5), + lowerLabelBackground.rightAnchor.constraint(equalTo: rightAnchor, constant: -5), + upperLabelBackground.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5), + upperLabelBackground.rightAnchor.constraint(equalTo: rightAnchor, constant: -5) + ]) + + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.strokeColor = gridLineColor.cgColor + shapeLayer.lineDashPattern = [2, 3] + shapeLayer.lineWidth = 1 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + if upperBound > 0 && lowerBound > 0 { + updateGrid() + } + + lowerLabelBackground.layer.cornerRadius = lowerLabelBackground.frame.height / 2 + upperLabelBackground.layer.cornerRadius = upperLabelBackground.frame.height / 2 + } + + static func makeLabel() -> UILabel { + let label = UILabel() + label.font = ChartYAxisInnerView.font + label.transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1) + return label + } + + func setBounds(lower: CGFloat, upper: CGFloat, lowerLabelText: String, upperLabelText: String, steps: [CGFloat]) { + lowerBound = lower + upperBound = upper + lowerLabel.text = lowerLabelText + upperLabel.text = upperLabelText + self.steps = steps + + updateGrid() + } + + func updateBounds(lower: CGFloat, upper: CGFloat, animationStyle: ChartAnimation = .none) { + lowerBound = lower + upperBound = upper + updateGrid(animationStyle: animationStyle) + } + + func updateGrid(animationStyle: ChartAnimation = .none) { + let p = UIBezierPath() + for step in steps { + p.move(to: CGPoint(x: 0, y: step)) + p.addLine(to: CGPoint(x: bounds.width, y: step)) + } + + let realPath = p + + let yScale = (bounds.height) / CGFloat(upperBound - lowerBound) + let yTranslate = (bounds.height) * CGFloat(-lowerBound) / CGFloat(upperBound - lowerBound) + let scale = CGAffineTransform.identity.scaledBy(x: 1, y: yScale) + let translate = CGAffineTransform.identity.translatedBy(x: 0, y: yTranslate) + let transform = scale.concatenating(translate) + realPath.apply(transform) + + if animationStyle != .none { + let timingFunction = CAMediaTimingFunction(name: animationStyle == .interactive ? .linear : .easeInEaseOut) + if shapeLayer.animationKeys()?.contains("path") ?? false, + let presentation = shapeLayer.presentation(), + let path = presentation.path { + shapeLayer.removeAnimation(forKey: "path") + shapeLayer.path = path + } + + let animation = CABasicAnimation(keyPath: "path") + let duration = animationStyle.rawValue + animation.duration = duration + animation.fromValue = shapeLayer.path + animation.timingFunction = timingFunction + layer.add(animation, forKey: "path") + } + + shapeLayer.path = realPath.cgPath + } +} + +class ChartYAxisView: UIView { + var lowerBound: CGFloat = 0 + var upperBound: CGFloat = 0 + var alignment: ChartYAxisViewAlignment = .right + var textColor: UIColor? + + var gridColor: UIColor = UIColor(white: 0, alpha: 0.3) { + didSet { + gridView?.gridColor = gridColor + } + } + + var gridLineColor: UIColor = UIColor(white: 0, alpha: 0.3) { + didSet { + gridView?.gridLineColor = gridLineColor + } + } + + override var frame: CGRect { + didSet { + gridView?.updateGrid() + } + } + + private var gridView: ChartYAxisInnerView? + + func setBounds(lower: CGFloat, + upper: CGFloat, + lowerLabel: String, + upperLabel: String, + steps: [CGFloat], + animationStyle: ChartAnimation = .none) { + let gv = ChartYAxisInnerView() + gv.alignment = alignment + gv.textColor = textColor + gv.gridColor = gridColor + gv.gridLineColor = gridLineColor + gv.frame = bounds + gv.autoresizingMask = [.flexibleWidth, .flexibleHeight] + addSubview(gv) + + if let gridView = gridView { + if animationStyle == .animated { + gv.setBounds(lower: lowerBound, + upper: upperBound, + lowerLabelText: lowerLabel, + upperLabelText: upperLabel, + steps: steps) + gv.alpha = 0 + gv.updateBounds(lower: lower, upper:upper, animationStyle: animationStyle) + gridView.updateBounds(lower: lower, upper:upper, animationStyle: animationStyle) + UIView.animate(withDuration: animationStyle.rawValue, animations: { + gv.alpha = 1 + gridView.alpha = 0 + }) { _ in + gridView.removeFromSuperview() + } + } else { + gv.setBounds(lower: lower, upper: upper, lowerLabelText: lowerLabel, upperLabelText: upperLabel, steps: steps) + gridView.removeFromSuperview() + } + } else { + gv.setBounds(lower: lower, upper: upper, lowerLabelText: lowerLabel, upperLabelText: upperLabel, steps: steps) + } + + gridView = gv + lowerBound = lower + upperBound = upper + } +} diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 8212cfef6f..bd7ab226dc 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -345,6 +345,7 @@ 472E3F4C2147D5700020E412 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472E3F4B2147D5700020E412 /* Subscription.swift */; }; 473464A7218B0BC000D6AF5B /* MWMPurchaseValidation.mm in Sources */ = {isa = PBXBuildFile; fileRef = 473464A6218B0BC000D6AF5B /* MWMPurchaseValidation.mm */; }; 4735008A23A83CF700661A95 /* DownloadedMapsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4735008923A83CF700661A95 /* DownloadedMapsDataSource.swift */; }; + 47375E572420ECA900FFCC49 /* Chart.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47375E562420ECA800FFCC49 /* Chart.framework */; }; 4738A8E7239FC513007C0F43 /* AdBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4738A8E6239FC513007C0F43 /* AdBannerView.swift */; }; 4738A8E9239FC526007C0F43 /* AdBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4738A8E8239FC526007C0F43 /* AdBannerView.xib */; }; 473CBF9B2164DD470059BD54 /* SettingsTableViewSelectableProgressCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 473CBF9A2164DD470059BD54 /* SettingsTableViewSelectableProgressCell.swift */; }; @@ -1434,6 +1435,7 @@ 473464A6218B0BC000D6AF5B /* MWMPurchaseValidation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MWMPurchaseValidation.mm; sourceTree = ""; }; 4735008923A83CF700661A95 /* DownloadedMapsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadedMapsDataSource.swift; sourceTree = ""; }; 473500C023A8F81800661A95 /* MWMFrameworkObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMFrameworkObserver.h; sourceTree = ""; }; + 47375E562420ECA800FFCC49 /* Chart.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Chart.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4738A8E6239FC513007C0F43 /* AdBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdBannerView.swift; sourceTree = ""; }; 4738A8E8239FC526007C0F43 /* AdBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AdBannerView.xib; sourceTree = ""; }; 473CBF9A2164DD470059BD54 /* SettingsTableViewSelectableProgressCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewSelectableProgressCell.swift; sourceTree = ""; }; @@ -2119,6 +2121,7 @@ buildActionMask = 2147483647; files = ( 47D48BD0242A475700FEFB1F /* AuthenticationServices.framework in Frameworks */, + 47375E572420ECA900FFCC49 /* Chart.framework in Frameworks */, 39CDE69123E1B6C8007CDA58 /* libge0.a in Frameworks */, 47A65CAD2350044800DCD85F /* CoreApi.framework in Frameworks */, 4577B28121F2066A00864FAC /* libvulkan_wrapper.a in Frameworks */, @@ -2299,6 +2302,7 @@ isa = PBXGroup; children = ( 47D48BCF242A475700FEFB1F /* AuthenticationServices.framework */, + 47375E562420ECA800FFCC49 /* Chart.framework */, 39CDE69023E1B6C8007CDA58 /* libge0.a */, 4740184123F5BDD300A93C81 /* minizip.framework */, 450B5C822355F50200E9019E /* libweb_api.a */, diff --git a/xcode/omim.xcworkspace/contents.xcworkspacedata b/xcode/omim.xcworkspace/contents.xcworkspacedata index b6fc79cdd2..1849d51d8d 100644 --- a/xcode/omim.xcworkspace/contents.xcworkspacedata +++ b/xcode/omim.xcworkspace/contents.xcworkspacedata @@ -196,6 +196,9 @@ + +