Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions OsmAnd.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "ic_custom_activity.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1814,6 +1814,7 @@
"select_tracks" = "Select tracks";
"selected_tracks_count" = "Tracks - %d";
"folder_tracks_count" = "%d tracks";
"folder_one_track" = "1 track";

"delete_tracks_bottom_sheet_description_regular_part" = "Are you sure you want to delete ";
"delete_tracks_bottom_sheet_description_bold_part" = "%d tracks?";
Expand Down Expand Up @@ -4560,3 +4561,25 @@
"missing_maps_header" = "Missing or outdated maps";
"missing_maps_description" = "Some offline maps required for this route are missing or not up to date. Please download or update these maps.";
"shared_string_allow" = "Allow";
"organize_by" = "Organize by";
"organize_by_summary" = "Automatically organize your tracks into groups based on a selected parameter like activity, speed, or location.";
"group_general" = "General";
"group_date_time" = "Date & Time";
"group_altitude_elevation" = "Altitude and elevation";
"group_sensors" = "Sensors";
"shared_string_length" = "Length";
"year_of_creation" = "Year of creation";
"month_year_creation" = "Month and Year of creation";
"nearest_city" = "Nearest city";
"organize_by_max_speed" = "Max. speed";
"avg_speed" = "Avg. speed";
"organize_by_max_altitude" = "Max. altitude";
"shared_string_avg_altitude" = "Avg. altitude";
"shared_string_uphill" = "Uphill";
"shared_string_downhill" = "Downhill";
"sort_highest_first" = "Highest first";
"sort_lowest_first" = "Lowest first";
"set_step_size" = "Set step size";
"set_step_size_summary" = "Choose a step size to group your tracks.";
"shared_string_step" = "Step";
"shared_string_unlock" = "Unlock";
240 changes: 240 additions & 0 deletions Sources/Controllers/MyPlaces/OrganizeByStepSizeViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import UIKit
import OsmAndShared

protocol OrganizeByStepSizeDelegate: AnyObject {
func onStepSizeChanged()
}

final class OrganizeByStepSizeViewController: OABaseNavbarViewController {

// MARK: - Private Types

private enum Section: Int {
case main
}

private enum RowKey: String {
case slider
}

private enum DataKey: String {
case stepValue
}

// MARK: - Instance Properties

private static let previewDebounceInterval: TimeInterval = 0.15

private let smartFolder: SmartFolder
private let type: OrganizeByType
private let displayUnits: any MeasurementUnit
private let stepRange: Limits
private let initialParams: OrganizeByParams?
private var currentDisplayValue: Float
private var isExplicitlyDismissed = false
private var previewWorkItem: DispatchWorkItem?

weak var stepDelegate: OrganizeByStepSizeDelegate?

// MARK: - Initializers

init(smartFolder: SmartFolder, type: OrganizeByType, originalParams: OrganizeByParams?) {
self.smartFolder = smartFolder
self.type = type
self.displayUnits = type.getDisplayUnits()
guard let range = type.stepRange else {
fatalError("OrganizeByStepSizeViewController requires a range-related OrganizeByType")
}
self.stepRange = range
self.initialParams = originalParams

let existingBase: Double
if let rangeParams = smartFolder.organizeByParams as? OrganizeByRangeParams {
existingBase = rangeParams.stepSize
} else {
existingBase = type.getDefaultStepInBaseUnits()
}
let rawDisplayValue = Float(Int(type.getDisplayUnits().fromBase(value: existingBase)))
let minDisplayValue = (range.min as? NSNumber).map { Float(truncating: $0) } ?? rawDisplayValue
let maxDisplayValue = (range.max as? NSNumber).map { Float(truncating: $0) } ?? rawDisplayValue
self.currentDisplayValue = min(max(rawDisplayValue, minDisplayValue), maxDisplayValue)

super.init()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Methods

override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableView.automaticDimension
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.presentationController?.delegate = self
}

override func registerCells() {
addCell(TopBottomValuesSliderTableViewCell.reuseIdentifier)
}

override func getTitle() -> String {
localizedString("set_step_size")
}

override func getTableHeaderDescription() -> String { "" }

override func getTableHeaderDescriptionAttr() -> NSAttributedString? {
let text = localizedString("set_step_size_summary")
return NSAttributedString(string: text, attributes: [
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: UIColor.textColorSecondary
])
}

override func systemLeftBarButtonItem() -> UIBarButtonItem? {
UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(onClosePressed))
}

override func tableStyle() -> UITableView.Style {
.insetGrouped
}

override func systemRightBarButtonItems() -> [UIBarButtonItem]? {
let isFeatureAccessible = !type.isPro || OAIAPHelper.isOsmAndProAvailable()
if isFeatureAccessible {
let image = UIImage.templateImageNamed("ic_checkmark_default")?.withTintColor(.white, renderingMode: .alwaysOriginal)
return [UIBarButtonItem(image: image, style: .done, target: self, action: #selector(onConfirmPressed))]
} else {
return [UIBarButtonItem(title: localizedString("shared_string_unlock"), style: .done, target: self, action: #selector(onUnlockPressed))]
}
}

override func generateData() {
tableData.clearAllData()
let section = tableData.createNewSection()
let row = section.createNewRow()
row.cellType = TopBottomValuesSliderTableViewCell.reuseIdentifier
row.key = RowKey.slider.rawValue
row.title = localizedString("shared_string_step")
row.setObj(currentDisplayValue as Any, forKey: DataKey.stepValue.rawValue)
}

override func getRow(_ indexPath: IndexPath?) -> UITableViewCell? {
guard let indexPath else { return nil }
let item = tableData.item(for: indexPath)
guard item.key == RowKey.slider.rawValue,
let cell = tableView.dequeueReusableCell(
withIdentifier: TopBottomValuesSliderTableViewCell.reuseIdentifier
) as? TopBottomValuesSliderTableViewCell,
let value = item.obj(forKey: DataKey.stepValue.rawValue) as? Float else { return nil }

let unitSymbol = displayUnits.getSymbol()
let minVal = (stepRange.min as? NSNumber).map { Float(truncating: $0) } ?? 0
let maxVal = (stepRange.max as? NSNumber).map { Float(truncating: $0) } ?? 100

cell.selectionStyle = .none
cell.topRightLabelVisibility(true)
cell.topRightButtonVisibility(false)
cell.sliderValuesVisibility(true)

cell.topLeftLabel.text = item.title
cell.topLeftLabel.font = .preferredFont(forTextStyle: .body)
cell.topRightLabel.text = "\(Int(value)) \(unitSymbol)"
cell.topRightLabel.textColor = .textColorSecondary

cell.slider.minimumValue = minVal
cell.slider.maximumValue = maxVal
cell.slider.value = value
cell.slider.accessibilityLabel = item.title
cell.slider.accessibilityValue = "\(Int(value)) \(unitSymbol)"
cell.slider.tintColor = .systemBlue
cell.slider.maximumTrackTintColor = .sliderLineBg
cell.slider.removeTarget(self, action: nil, for: .valueChanged)
cell.slider.addTarget(self, action: #selector(onSliderChanged(_:)), for: .valueChanged)

cell.bottomLeftLabel.text = "\(Int(minVal)) \(unitSymbol)"
cell.bottomLeftLabel.textColor = .textColorSecondary
cell.bottomRightLabel.text = "\(Int(maxVal)) \(unitSymbol)"
cell.bottomRightLabel.textColor = .textColorSecondary

return cell
}

private func makeCurrentParams() -> OrganizeByRangeParams {
let stepInBase = displayUnits.toBase(value: Double(Int(currentDisplayValue)))
return OrganizeByRangeParams(type: type, stepSize: stepInBase)
}

private func applyStepInMemory() {
smartFolder.setOrganizeByParams(organizeByParams: makeCurrentParams())
}

private func persistCurrentStep() {
let params = makeCurrentParams()
SharedLibSmartFolderHelper.shared.setOrganizeByParams(folderId: smartFolder.getId(), params: params)
smartFolder.setOrganizeByParams(organizeByParams: params)
}

private func schedulePreviewUpdate() {
previewWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.applyStepInMemory()
self.stepDelegate?.onStepSizeChanged()
}
previewWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + Self.previewDebounceInterval, execute: workItem)
}

private func performCancel() {
previewWorkItem?.cancel()
SharedLibSmartFolderHelper.shared.setOrganizeByParams(folderId: smartFolder.getId(), params: initialParams)
smartFolder.setOrganizeByParams(organizeByParams: initialParams)
stepDelegate?.onStepSizeChanged()
}

@objc private func onSliderChanged(_ sender: UISlider) {
let rounded = sender.value.rounded()
sender.value = rounded
currentDisplayValue = rounded
if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: Section.main.rawValue)) as? TopBottomValuesSliderTableViewCell {
let formattedValue = "\(Int(rounded)) \(displayUnits.getSymbol())"
cell.topRightLabel.text = formattedValue
cell.slider.accessibilityValue = formattedValue
}
schedulePreviewUpdate()
}

@objc private func onConfirmPressed() {
isExplicitlyDismissed = true
previewWorkItem?.cancel()
persistCurrentStep()
stepDelegate?.onStepSizeChanged()
dismiss(animated: true)
}

@objc private func onClosePressed() {
isExplicitlyDismissed = true
performCancel()
dismiss(animated: true)
}

@objc private func onUnlockPressed() {
guard let navigationController else { return }
OAChoosePlanHelper.showChoosePlanScreen(with: OAFeature.advanced_WIDGETS(), navController: navigationController)
}
}

// MARK: - UIAdaptivePresentationControllerDelegate

extension OrganizeByStepSizeViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
guard !isExplicitlyDismissed else { return }
performCancel()
}
}
Loading