Improve route statistics

This commit is contained in:
Christoph Hagen
2025-05-02 14:54:41 +02:00
parent 3b2cc75fc3
commit fea06a93b7
9 changed files with 207 additions and 67 deletions

View File

@ -200,7 +200,7 @@
E2EC1FAB2DC0C99600C41784 /* RouteViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */; }; E2EC1FAB2DC0C99600C41784 /* RouteViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */; };
E2EC1FAD2DC0D2FA00C41784 /* RouteLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */; }; E2EC1FAD2DC0D2FA00C41784 /* RouteLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */; };
E2EC1FB02DC0D7DA00C41784 /* RouteBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */; }; E2EC1FB02DC0D7DA00C41784 /* RouteBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */; };
E2EC1FB22DC0D8BD00C41784 /* RouteViewComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */; }; E2EC1FB22DC0D8BD00C41784 /* RouteStatisticType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB12DC0D8BD00C41784 /* RouteStatisticType.swift */; };
E2EC1FB42DC0FA8700C41784 /* Insert+Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */; }; E2EC1FB42DC0FA8700C41784 /* Insert+Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */; };
E2F3B3832DC496CB00CFA712 /* GalleryBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3822DC496C800CFA712 /* GalleryBlock.swift */; }; E2F3B3832DC496CB00CFA712 /* GalleryBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3822DC496C800CFA712 /* GalleryBlock.swift */; };
E2F3B3852DC49B7A00CFA712 /* Insert+Gallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3842DC49B4400CFA712 /* Insert+Gallery.swift */; }; E2F3B3852DC49B7A00CFA712 /* Insert+Gallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3842DC49B4400CFA712 /* Insert+Gallery.swift */; };
@ -480,7 +480,7 @@
E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViews.swift; sourceTree = "<group>"; }; E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViews.swift; sourceTree = "<group>"; };
E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteLocalization.swift; sourceTree = "<group>"; }; E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteLocalization.swift; sourceTree = "<group>"; };
E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteBlock.swift; sourceTree = "<group>"; }; E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteBlock.swift; sourceTree = "<group>"; };
E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViewComponents.swift; sourceTree = "<group>"; }; E2EC1FB12DC0D8BD00C41784 /* RouteStatisticType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteStatisticType.swift; sourceTree = "<group>"; };
E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Route.swift"; sourceTree = "<group>"; }; E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Route.swift"; sourceTree = "<group>"; };
E2F3B3822DC496C800CFA712 /* GalleryBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryBlock.swift; sourceTree = "<group>"; }; E2F3B3822DC496C800CFA712 /* GalleryBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryBlock.swift; sourceTree = "<group>"; };
E2F3B3842DC49B4400CFA712 /* Insert+Gallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Gallery.swift"; sourceTree = "<group>"; }; E2F3B3842DC49B4400CFA712 /* Insert+Gallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Gallery.swift"; sourceTree = "<group>"; };
@ -1081,7 +1081,7 @@
E2EC1FAE2DC0D30100C41784 /* Routes */ = { E2EC1FAE2DC0D30100C41784 /* Routes */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */, E2EC1FB12DC0D8BD00C41784 /* RouteStatisticType.swift */,
E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */, E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */,
E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */, E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */,
); );
@ -1456,7 +1456,7 @@
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
E2FD1D682D483CCF00B48627 /* Insert+Buttons.swift in Sources */, E2FD1D682D483CCF00B48627 /* Insert+Buttons.swift in Sources */,
E2EC1FB22DC0D8BD00C41784 /* RouteViewComponents.swift in Sources */, E2EC1FB22DC0D8BD00C41784 /* RouteStatisticType.swift in Sources */,
E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */,
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */,
E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */,

View File

@ -33,14 +33,27 @@ struct RouteBlock: KeyedBlockProcessor {
} }
func process(_ arguments: [Key : String], markdown: Substring) -> String { func process(_ arguments: [Key : String], markdown: Substring) -> String {
let rawComponents = arguments[.components] ?? "all"
guard let imageId = arguments[.image], guard let imageId = arguments[.image],
let fileId = arguments[.file], let fileId = arguments[.file] else {
let components = RouteViewComponents(rawValue: rawComponents) else {
invalid(markdown) invalid(markdown)
return "" return ""
} }
let rawComponents = arguments[.components]
var displayedTypes: Set<RouteStatisticType> = []
if let rawComponents {
rawComponents.components(separatedBy: ",").compactMap { $0.trimmed.nonEmpty }.forEach { rawType in
if let type = RouteStatisticType(rawValue: rawType) {
displayedTypes.insert(type)
} else {
results.warning("Unknown component type '\(rawType)' in route block")
}
}
}
if displayedTypes.isEmpty {
displayedTypes = Set(RouteStatisticType.allCases)
}
guard let image = content.image(imageId) else { guard let image = content.image(imageId) else {
results.missing(file: imageId, source: "Route block") results.missing(file: imageId, source: "Route block")
return "" return ""
@ -66,7 +79,7 @@ struct RouteBlock: KeyedBlockProcessor {
localization: language == .english ? .english : .german, localization: language == .english ? .english : .german,
chartTitle: arguments[.chartTitle], chartTitle: arguments[.chartTitle],
chartId: "chart-" + id, chartId: "chart-" + id,
components: components, displayedTypes: displayedTypes,
mapTitle: arguments[.mapTitle], mapTitle: arguments[.mapTitle],
mapId: "map-" + id, mapId: "map-" + id,
filePath: file.absoluteUrl, filePath: file.absoluteUrl,
@ -76,6 +89,7 @@ struct RouteBlock: KeyedBlockProcessor {
caption: arguments[.caption]) caption: arguments[.caption])
results.require(footer: views.script) results.require(footer: views.script)
results.require(icons: displayedTypes.map { $0.icon })
return views.content return views.content
} }

View File

@ -33,6 +33,12 @@ enum PageIcon: String, CaseIterable {
case statisticsEnergy = "energy" case statisticsEnergy = "energy"
case statisticsStopwatch = "stopwatch"
case statisticsHeart = "heart-pulse"
case statisticsSpeedometer = "speedometer"
// MARK: Buttons // MARK: Buttons
case buttonDownload = "download" case buttonDownload = "download"
@ -68,6 +74,9 @@ enum PageIcon: String, CaseIterable {
case .statisticsElevationDown: Icon.Statistics.ElevationDown.self case .statisticsElevationDown: Icon.Statistics.ElevationDown.self
case .statisticsDistance: Icon.Statistics.Distance.self case .statisticsDistance: Icon.Statistics.Distance.self
case .statisticsEnergy: Icon.Statistics.Energy.self case .statisticsEnergy: Icon.Statistics.Energy.self
case .statisticsStopwatch: Icon.Statistics.Stopwatch.self
case .statisticsHeart: Icon.Statistics.HeartPulse.self
case .statisticsSpeedometer: Icon.Statistics.Speedometer.self
case .buttonDownload: Icon.ArrowDown.self case .buttonDownload: Icon.ArrowDown.self
case .buttonExternalLink: Icon.ArrowRight.self case .buttonExternalLink: Icon.ArrowRight.self
case .buttonGitLink: Icon.Git.self case .buttonGitLink: Icon.Git.self
@ -113,11 +122,14 @@ enum PageIcon: String, CaseIterable {
case .audioPlayerPrevious: "Audio Player: Previous" case .audioPlayerPrevious: "Audio Player: Previous"
case .audioPlayerNext: "Audio Player: Next" case .audioPlayerNext: "Audio Player: Next"
case .buttonDownload: "Button: Download" case .buttonDownload: "Button: Download"
case .statisticsTime: "Time" case .statisticsTime: "Clock (Duration)"
case .statisticsElevationUp: "Elevation Up" case .statisticsElevationUp: "Arrow Up (Elevation Up)"
case .statisticsElevationDown: "Elevation Down" case .statisticsElevationDown: "Arrow Down (Elevation Down)"
case .statisticsDistance: "Distance" case .statisticsDistance: "Signpost (Distance)"
case .statisticsEnergy: "Energy / Calories" case .statisticsEnergy: "Flame (Energy / Calories)"
case .statisticsStopwatch: "Stopwatch (Pace)"
case .statisticsHeart: "Heart Rate"
case .statisticsSpeedometer: "Speedometer (Speed)"
} }
} }

View File

@ -7,7 +7,7 @@ extension Icon {
static let id = "icon-clock" static let id = "icon-clock"
static let attributes = "viewBox='0 0 16 16' width='16' height='16'" static let attributes = "viewBox='0 0 16 16'"
static let content = static let content =
""" """
@ -15,11 +15,24 @@ extension Icon {
""" """
} }
/// [Source](https://icons.getbootstrap.com/icons/stopwatch/)
struct Stopwatch: ContentIcon {
static let id = "icon-stopwatch"
static let attributes = "viewBox='0 0 16 16'"
static let content =
"""
<path fill="currentColor" d="M8.5 5.6a.5.5 0 1 0-1 0v2.9h-3a.5.5 0 0 0 0 1H8a.5.5 0 0 0 .5-.5z"/><path fill="currentColor" d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64l.012-.013.354-.354-.354-.353a.5.5 0 0 1 .707-.708l1.414 1.415a.5.5 0 1 1-.707.707l-.353-.354-.354.354-.013.012A7 7 0 1 1 7 2.071V1.5a.5.5 0 0 1-.5-.5M8 3a6 6 0 1 0 .001 12A6 6 0 0 0 8 3"/>
"""
}
struct ElevationUp: ContentIcon { struct ElevationUp: ContentIcon {
static let id = "icon-elevation-up" static let id = "icon-elevation-up"
static let attributes = "width='16' height='16'" static let attributes = "viewBox='0 0 16 16'"
static let content = static let content =
""" """
@ -31,7 +44,7 @@ extension Icon {
static let id = "icon-elevation-down" static let id = "icon-elevation-down"
static let attributes = "width='16' height='16'" static let attributes = "viewBox='0 0 16 16'"
static let content = static let content =
""" """
@ -43,7 +56,7 @@ extension Icon {
static let id = "icon-distance" static let id = "icon-distance"
static let attributes = "width='16' height='16'" static let attributes = "viewBox='0 0 16 16'"
static let content = static let content =
""" """
@ -55,12 +68,38 @@ extension Icon {
static let id = "icon-energy" static let id = "icon-energy"
static let attributes = "width='16' height='16'" static let attributes = "viewBox='0 0 16 16'"
static let content = static let content =
""" """
<path fill="currentColor" d="M8 16c3.3 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .3 1.5-1.3 2-1.3 2C11 4 9 .5 6 0c.4 2 .5 4-2 6-1.3 1-2 2.7-2 4.5C2 14 4.7 16 8 16Zm0-1c-1.7 0-3-1-3-2.8 0-.7.3-2 1.3-3-.2.8.7 1.3.7 1.3-.4-1.3.5-3.3 2-3.5-.2 1-.3 2 1 3a3 3 0 0 1 1 2.3C11 14 9.7 15 8 15Z"/> <path fill="currentColor" d="M8 16c3.3 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .3 1.5-1.3 2-1.3 2C11 4 9 .5 6 0c.4 2 .5 4-2 6-1.3 1-2 2.7-2 4.5C2 14 4.7 16 8 16Zm0-1c-1.7 0-3-1-3-2.8 0-.7.3-2 1.3-3-.2.8.7 1.3.7 1.3-.4-1.3.5-3.3 2-3.5-.2 1-.3 2 1 3a3 3 0 0 1 1 2.3C11 14 9.7 15 8 15Z"/>
""" """
} }
/// [Source](https://icons.getbootstrap.com/icons/heart-pulse/)
struct HeartPulse: ContentIcon {
static let id = "icon-pulse"
static let attributes = "viewBox='0 0 16 16'"
static let content =
"""
<path fill="currentColor" d="m8 2.748-.717-.737C5.6.281 2.514.878 1.4 3.053.918 3.995.78 5.323 1.508 7H.43c-2.128-5.697 4.165-8.83 7.394-5.857q.09.083.176.171a3 3 0 0 1 .176-.17c3.23-2.974 9.522.159 7.394 5.856h-1.078c.728-1.677.59-3.005.108-3.947C13.486.878 10.4.28 8.717 2.01zM2.212 10h1.315C4.593 11.183 6.05 12.458 8 13.795c1.949-1.337 3.407-2.612 4.473-3.795h1.315c-1.265 1.566-3.14 3.25-5.788 5-2.648-1.75-4.523-3.434-5.788-5"/><path fill="currentColor" d="M10.464 3.314a.5.5 0 0 0-.945.049L7.921 8.956 6.464 5.314a.5.5 0 0 0-.88-.091L3.732 8H.5a.5.5 0 0 0 0 1H4a.5.5 0 0 0 .416-.223l1.473-2.209 1.647 4.118a.5.5 0 0 0 .945-.049l1.598-5.593 1.457 3.642A.5.5 0 0 0 12 9h3.5a.5.5 0 0 0 0-1h-3.162z"/>
"""
}
/// [Source](https://icons.getbootstrap.com/icons/speedometer/)
struct Speedometer: ContentIcon {
static let id = "icon-speed"
static let attributes = "viewBox='0 0 16 16'"
static let content =
"""
<path fill="currentColor" d="M8 2a.5.5 0 0 1 .5.5V4a.5.5 0 0 1-1 0V2.5A.5.5 0 0 1 8 2M3.732 3.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707M2 8a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 8m9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5m.754-4.246a.39.39 0 0 0-.527-.02L7.547 7.31A.91.91 0 1 0 8.85 8.569l3.434-4.297a.39.39 0 0 0-.029-.518z"/><path fill="currentColor" fill-rule="evenodd" d="M6.664 15.889A8 8 0 1 1 9.336.11a8 8 0 0 1-2.672 15.78zm-4.665-4.283A11.95 11.95 0 0 1 8 10c2.186 0 4.236.585 6.001 1.606a7 7 0 1 0-12.002 0"/>
"""
}
} }
} }

View File

@ -1,13 +1,7 @@
struct RouteLocalization { struct RouteLocalization {
let elevation: String let statistics: [RouteStatisticType : String]
let speed: String
let pace: String
let heartRate: String
let fallback: String let fallback: String
@ -25,10 +19,13 @@ struct RouteLocalization {
extension RouteLocalization { extension RouteLocalization {
static let german: RouteLocalization = .init( static let german: RouteLocalization = .init(
elevation: "Höhe", statistics: [
speed: "Geschw.", .elevation: "Höhe",
pace: "Pace", .speed: "Geschwindigkeit",
heartRate: "Herzfrequenz", .pace: "Pace",
.heartRate: "Herzfrequenz",
.energy: "Aktive Kalorien"
],
fallback: "Zur Anzeige der Statistiken wird JavaScript und Unterstützung für HTML5 Canvas benötigt.", fallback: "Zur Anzeige der Statistiken wird JavaScript und Unterstützung für HTML5 Canvas benötigt.",
hourUnit: "Std", hourUnit: "Std",
duration: "Dauer", duration: "Dauer",
@ -37,10 +34,13 @@ extension RouteLocalization {
loadFail: "Die Statistiken konnten nicht geladen werden") loadFail: "Die Statistiken konnten nicht geladen werden")
static let english: RouteLocalization = .init( static let english: RouteLocalization = .init(
elevation: "Elevation", statistics: [
speed: "Speed", .elevation: "Elevation",
pace: "Pace", .speed: "Speed",
heartRate: "Heart Rate", .pace: "Pace",
.heartRate: "Heart Rate",
.energy: "Active Energy"
],
fallback: "Javascript and HTML5 Canvas Support are required to display statistics", fallback: "Javascript and HTML5 Canvas Support are required to display statistics",
hourUnit: "h", hourUnit: "h",
duration: "Duration", duration: "Duration",

View File

@ -0,0 +1,65 @@
enum RouteStatisticType: String, CaseIterable {
case elevation
case speed
case pace
case heartRate = "heart-rate"
case energy
var order: Int {
switch self {
case .elevation: 1
case .speed: 2
case .pace: 3
case .heartRate: 4
case .energy: 5
}
}
var id: String {
switch self {
case .elevation: "elevation"
case .speed: "speed"
case .pace: "pace"
case .heartRate: "hr"
case .energy: "energy"
}
}
var unit: String {
switch self {
case .elevation: "m"
case .speed: "km/h"
case .pace: "min/km"
case .heartRate: "bpm"
case .energy: "kcal"
}
}
var icon: PageIcon {
switch self {
case .elevation: .statisticsElevationUp
case .speed: .statisticsSpeedometer
case .pace: .statisticsStopwatch
case .heartRate: .statisticsHeart
case .energy: .statisticsEnergy
}
}
func displayText(in language: ContentLanguage) -> String {
let localization: RouteLocalization = language == .english ? .english : .german
return localization.statistics[self]!
}
}
extension RouteStatisticType: Comparable {
static func < (lhs: RouteStatisticType, rhs: RouteStatisticType) -> Bool {
lhs.order < rhs.order
}
}

View File

@ -1,7 +0,0 @@
enum RouteViewComponents: String {
case onlyElevation = "elevation"
case all = "all"
case withoutHeartRate = "no-hr"
}

View File

@ -6,7 +6,7 @@ struct RouteViews: HtmlProducer {
/// The HTML id attribute used to enable fullscreen images /// The HTML id attribute used to enable fullscreen images
let map: PageImage let map: PageImage
let components: RouteViewComponents let displayedTypes: Set<RouteStatisticType>
let mapId: String let mapId: String
@ -21,7 +21,7 @@ struct RouteViews: HtmlProducer {
init(localization: RouteLocalization, init(localization: RouteLocalization,
chartTitle: String?, chartTitle: String?,
chartId: String, chartId: String,
components: RouteViewComponents, displayedTypes: Set<RouteStatisticType>,
mapTitle: String?, mapTitle: String?,
mapId: String, mapId: String,
filePath: String, filePath: String,
@ -31,7 +31,7 @@ struct RouteViews: HtmlProducer {
caption: String? caption: String?
) { ) {
self.localization = localization self.localization = localization
self.components = components self.displayedTypes = displayedTypes
self.mapId = mapId self.mapId = mapId
self.filePath = filePath self.filePath = filePath
self.map = PageImage( self.map = PageImage(
@ -47,9 +47,9 @@ struct RouteViews: HtmlProducer {
self.mapTitle = mapTitle self.mapTitle = mapTitle
} }
var pickerHiddenText: String { private func button(series: RouteStatisticType) -> String {
guard components == .onlyElevation else { return "" } let label = localization.statistics[series]!
return " style='display:none'" return "<button data-type='\(series.id)' unit='\(series.unit)' name='\(label)'>\(series.icon.usageString)<span class='label'>\(label)</span></button>"
} }
func populate(_ result: inout String) { func populate(_ result: inout String) {
@ -58,18 +58,19 @@ struct RouteViews: HtmlProducer {
} }
map.populate(&result) map.populate(&result)
let series = displayedTypes.sorted()
guard !series.isEmpty else {
return
}
if let chartTitle { if let chartTitle {
result += "<h2>\(chartTitle)</h2>" result += "<h2>\(chartTitle)</h2>"
} }
result += "<div id='\(chartId)' class='charts'>" result += "<div id='\(chartId)' class='charts'>"
result += "<div class='picker y-picker'\(pickerHiddenText)>" result += "<div class='picker icon-picker y-picker'>"
result += "<button data-type='elevation' unit='m' class='active'>\(localization.elevation)</button>" for type in series {
result += "<button data-type='speed' unit='km/h'>\(localization.speed)</button>" result += button(series: type)
result += "<button data-type='pace' unit='min/km'>\(localization.pace)</button>"
if components == .all {
result += "<button data-type='hr' unit='bpm'>\(localization.heartRate)</button>"
} }
result += "</div>" result += "</div>" // Picker
result += "<div class='graph'>" result += "<div class='graph'>"
result += "<canvas>" result += "<canvas>"
result += "<div class='fallback'>\(localization.fallback)</div>" result += "<div class='fallback'>\(localization.fallback)</div>"
@ -77,13 +78,13 @@ struct RouteViews: HtmlProducer {
result += "<div class='line'></div>" result += "<div class='line'></div>"
result += "<div class='tooltip'></div>" result += "<div class='tooltip'></div>"
result += "<div class='load-error'>\(localization.loadFail)</div>" result += "<div class='load-error'>\(localization.loadFail)</div>"
result += "</div>" result += "</div>" // Graph
result += "<div class='picker x-picker'\(pickerHiddenText)>" result += "<div class='picker text-picker x-picker'>"
result += "<button data-type='distance' unit='km' class='active'>\(localization.distance)</button>" result += "<button data-type='distance' unit='km'>\(localization.distance)</button>"
result += "<button data-type='duration' unit='\(localization.hourUnit)'>\(localization.duration)</button>" result += "<button data-type='duration' unit='\(localization.hourUnit)'>\(localization.duration)</button>"
result += "<button data-type='time' unit=''>\(localization.time)</button>" result += "<button data-type='time' unit=''>\(localization.time)</button>"
result += "</div>" result += "</div>" // Picker
result += "</div>" result += "</div>" // Chart
} }
var script: String { var script: String {

View File

@ -3,6 +3,9 @@ import SFSafeSymbols
struct InsertableRoute: View, InsertableCommandView { struct InsertableRoute: View, InsertableCommandView {
@Environment(\.language)
var language
final class Model: ObservableObject, InsertableCommandModel { final class Model: ObservableObject, InsertableCommandModel {
@Published @Published
@ -12,7 +15,7 @@ struct InsertableRoute: View, InsertableCommandView {
var chartTitle: String? var chartTitle: String?
@Published @Published
var components: RouteViewComponents = .all var components: Set<RouteStatisticType> = Set(RouteStatisticType.allCases)
@Published @Published
var mapTitle: String? var mapTitle: String?
@ -39,7 +42,12 @@ struct InsertableRoute: View, InsertableCommandView {
var result = ["```route"] var result = ["```route"]
result.append("\(RouteBlock.Key.image.rawValue): \(selectedImage.id)") result.append("\(RouteBlock.Key.image.rawValue): \(selectedImage.id)")
result.append("\(RouteBlock.Key.file.rawValue): \(dataFile.id)") result.append("\(RouteBlock.Key.file.rawValue): \(dataFile.id)")
result.append("\(RouteBlock.Key.components.rawValue): \(components.rawValue)") if components != Set(RouteStatisticType.allCases) {
let list = components
.map { $0.rawValue }
.joined(separator: ", ")
result.append("\(RouteBlock.Key.components.rawValue): \(list)")
}
if let caption { if let caption {
result.append("\(RouteBlock.Key.caption.rawValue): \(caption)") result.append("\(RouteBlock.Key.caption.rawValue): \(caption)")
} }
@ -94,14 +102,22 @@ struct InsertableRoute: View, InsertableCommandView {
text: $model.chartTitle, text: $model.chartTitle,
prompt: "Title", prompt: "Title",
footer: "The title to show above the statistics") footer: "The title to show above the statistics")
Picker(selection: $model.components) { ForEach(RouteStatisticType.allCases.sorted(), id: \.rawValue) { type in
Text("All").tag(RouteViewComponents.all) Toggle(isOn: Binding(
Text("Only elevation").tag(RouteViewComponents.withoutHeartRate) get: {
Text("No heart rate").tag(RouteViewComponents.withoutHeartRate) model.components.contains(type)
} label: { },
Text("") set: { isSelected in
if isSelected {
model.components.insert(type)
} else {
model.components.remove(type)
}
}
)) {
Text(type.displayText(in: language))
}.toggleStyle(.checkbox)
} }
.pickerStyle(.segmented)
} }
} }
} }