mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Implement map legend overlay (#1653)
* Add map legend feature (issue #924) Implement a map legend overlay accessible from both the mesh map and node detail map views. The legend explains all visual map elements including: - Online/offline node markers with pulsing animation - Detection sensor nodes - Waypoints - Position precision circles - Position history points and heading arrows - Route start/end markers and route lines - Convex hull mesh coverage outline A new "map" button is added to the floating control buttons on both map views, opening the legend as a sheet. Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/23f75e1e-549b-46a1-84c9-fb0a6375dcd9 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve legend descriptions for online/offline nodes Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/23f75e1e-549b-46a1-84c9-fb0a6375dcd9 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * map button glass and cleanup * Update Meshtastic/Views/Nodes/Helpers/Map/MapLegend.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update incorect online timeframe * Update Meshtastic/Views/Nodes/MeshMap.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * translation file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
21794d004b
commit
bac376edcb
8 changed files with 446 additions and 19 deletions
|
|
@ -3118,6 +3118,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"A previous position report for this node." : {
|
||||
"comment" : "A description of a position history point.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"A previous position report showing the direction of travel." : {
|
||||
|
||||
},
|
||||
"A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : {
|
||||
"localizations" : {
|
||||
|
|
@ -3163,6 +3170,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"A shared point of interest. Long-press the map to create one." : {
|
||||
"comment" : "A description of a waypoint.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"A Trace Route was sent, no response has been received." : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -6009,6 +6020,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"An outline enclosing all LoRa node positions on the mesh." : {
|
||||
"comment" : "A description of the convex hull of a mesh.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Any missed messages will be delivered again." : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -14154,6 +14169,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Dashed line showing a recorded route path." : {
|
||||
"comment" : "A description of a dashed line that shows a recorded route path.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Date" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -25286,6 +25305,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hide legend" : {
|
||||
"comment" : "A label for a button that hides the map legend.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Hide map legend" : {
|
||||
|
||||
},
|
||||
"Hides the map legend." : {
|
||||
|
||||
},
|
||||
"HIGH" : {
|
||||
"localizations" : {
|
||||
|
|
@ -27578,6 +27607,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Indicates reduced GPS precision. The node is somewhere within the shaded area." : {
|
||||
"comment" : "A description of a position precision circle.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Indoor Air Quality" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -30697,6 +30730,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Map Legend" : {
|
||||
"comment" : "A title for a view that displays a map legend.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Map Options" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -31159,6 +31196,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Mesh Coverage" : {
|
||||
"comment" : "A heading for the convex hull of the mesh.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Mesh Live Activity" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -35253,6 +35294,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Node heard within the last 2 hours. Shown with a pulsing ring on the map." : {
|
||||
"comment" : "A description of the pulsing ring on the map.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Node History" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -35404,6 +35449,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Node not heard recently. Shown without a pulsing ring on the map." : {
|
||||
"comment" : "A description of a node that is not heard by the user.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Node Number" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -35456,6 +35505,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Node with an active detection sensor module." : {
|
||||
"comment" : "A description of a sensor node.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Nodes" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -36190,6 +36243,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Offline Node" : {
|
||||
"comment" : "A description of an offline node.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Ok" : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
@ -36808,6 +36865,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Online Node" : {
|
||||
"comment" : "A label displayed in the map legend for an online node.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role." : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -39546,6 +39607,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Position History" : {
|
||||
"comment" : "A heading for the position history of a node.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Position History Point" : {
|
||||
"comment" : "A label displayed in the map legend for a position history point.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Position Log" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -39684,6 +39753,13 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Position Precision" : {
|
||||
|
||||
},
|
||||
"Position Precision Circle" : {
|
||||
"comment" : "A description of a map element that indicates reduced GPS precision.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Position Sent" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -39736,6 +39812,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Position with Heading" : {
|
||||
"comment" : "A description of a position history point that shows the direction of travel.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Positions Enabled" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -44715,6 +44795,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Route End" : {
|
||||
"comment" : "A label for a route end point on a map.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Route Line" : {
|
||||
"comment" : "A description of a route line on a map.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Route Lines" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -44893,6 +44981,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Route Start" : {
|
||||
"comment" : "A label displayed for the start of a route.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Route: %@" : {
|
||||
"localizations" : {
|
||||
"da" : {
|
||||
|
|
@ -50389,6 +50481,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Show legend" : {
|
||||
"comment" : "A label for a button that shows/hides the map legend.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Show map legend" : {
|
||||
|
||||
},
|
||||
"Show nodes" : {
|
||||
"localizations" : {
|
||||
|
|
@ -50608,6 +50707,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Shows the map legend." : {
|
||||
|
||||
},
|
||||
"Shut Down" : {
|
||||
"localizations" : {
|
||||
|
|
@ -56164,6 +56266,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Toggles the map legend" : {
|
||||
"comment" : "A hint for the user to toggle the map legend.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Topic: %@" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
|
@ -60865,6 +60971,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Waypoint" : {
|
||||
"comment" : "A caption displayed underneath the name of a waypoint.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Waypoint Failed to Send" : {
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@
|
|||
DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; };
|
||||
DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; };
|
||||
DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; };
|
||||
DD09240001E7FAD600E70001 /* MapLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09240002E7FAD600E70001 /* MapLegend.swift */; };
|
||||
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; };
|
||||
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; };
|
||||
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; };
|
||||
|
|
@ -579,6 +580,7 @@
|
|||
DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = "<group>"; };
|
||||
DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = "<group>"; };
|
||||
DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsForm.swift; sourceTree = "<group>"; };
|
||||
DD09240002E7FAD600E70001 /* MapLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLegend.swift; sourceTree = "<group>"; };
|
||||
DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -1181,6 +1183,7 @@
|
|||
children = (
|
||||
DDDC22362BA9232C002C44F1 /* MapContent */,
|
||||
3D3417D32E2DC293006A988B /* MapDataFiles.swift */,
|
||||
DD09240002E7FAD600E70001 /* MapLegend.swift */,
|
||||
DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */,
|
||||
DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */,
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */,
|
||||
|
|
@ -1804,6 +1807,7 @@
|
|||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */,
|
||||
DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */,
|
||||
DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */,
|
||||
DD09240001E7FAD600E70001 /* MapLegend.swift in Sources */,
|
||||
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */,
|
||||
23AD546B2E2AA5A80046E9AB /* AccessoryManager+ToRadio.swift in Sources */,
|
||||
23F488122E32980B002C776F /* AccessoryManager+Position.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,18 @@ extension View {
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func glassButtonStyle() -> some View {
|
||||
if #available(iOS 26.0, macOS 26.0, *) {
|
||||
self.buttonStyle(.glass)
|
||||
} else {
|
||||
self
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct FirstAppear: ViewModifier {
|
||||
|
|
|
|||
277
Meshtastic/Views/Nodes/Helpers/Map/MapLegend.swift
Normal file
277
Meshtastic/Views/Nodes/Helpers/Map/MapLegend.swift
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
//
|
||||
// MapLegend.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Implements a map legend overlay that explains the visual elements
|
||||
// displayed on the map (issue #924).
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MapLegendItem: View {
|
||||
let symbol: AnyView
|
||||
let title: String
|
||||
let subtitle: String?
|
||||
|
||||
init(symbol: AnyView, title: String, subtitle: String? = nil) {
|
||||
self.symbol = symbol
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
symbol
|
||||
.frame(width: 40, height: 40)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MapLegend: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
let isMeshMap: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
nodeSection
|
||||
if isMeshMap {
|
||||
waypointSection
|
||||
}
|
||||
precisionSection
|
||||
if !isMeshMap {
|
||||
historySection
|
||||
}
|
||||
routeSection
|
||||
if isMeshMap {
|
||||
convexHullSection
|
||||
}
|
||||
}
|
||||
.navigationTitle("Map Legend")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sections
|
||||
|
||||
private var nodeSection: some View {
|
||||
Section {
|
||||
MapLegendItem(
|
||||
symbol: AnyView(onlineNodeSymbol),
|
||||
title: String(localized: "Online Node"),
|
||||
subtitle: String(localized: "Node heard within the last 2 hours. Shown with a pulsing ring on the map.")
|
||||
)
|
||||
MapLegendItem(
|
||||
symbol: AnyView(offlineNodeSymbol),
|
||||
title: String(localized: "Offline Node"),
|
||||
subtitle: String(localized: "Node not heard recently. Shown without a pulsing ring on the map.")
|
||||
)
|
||||
MapLegendItem(
|
||||
symbol: AnyView(sensorNodeSymbol),
|
||||
title: String(localized: "Detection Sensor"),
|
||||
subtitle: String(localized: "Node with an active detection sensor module.")
|
||||
)
|
||||
} header: {
|
||||
Text("Nodes")
|
||||
}
|
||||
}
|
||||
|
||||
private var waypointSection: some View {
|
||||
Section {
|
||||
MapLegendItem(
|
||||
symbol: AnyView(waypointSymbol),
|
||||
title: String(localized: "Waypoint"),
|
||||
subtitle: String(localized: "A shared point of interest. Long-press the map to create one.")
|
||||
)
|
||||
} header: {
|
||||
Text("Waypoints")
|
||||
}
|
||||
}
|
||||
|
||||
private var precisionSection: some View {
|
||||
Section {
|
||||
MapLegendItem(
|
||||
symbol: AnyView(precisionCircleSymbol),
|
||||
title: String(localized: "Position Precision Circle"),
|
||||
subtitle: String(localized: "Indicates reduced GPS precision. The node is somewhere within the shaded area.")
|
||||
)
|
||||
} header: {
|
||||
Text("Position Precision")
|
||||
}
|
||||
}
|
||||
|
||||
private var historySection: some View {
|
||||
Section {
|
||||
MapLegendItem(
|
||||
symbol: AnyView(historyPointSymbol),
|
||||
title: String(localized: "Position History Point"),
|
||||
subtitle: String(localized: "A previous position report for this node.")
|
||||
)
|
||||
MapLegendItem(
|
||||
symbol: AnyView(historyArrowSymbol),
|
||||
title: String(localized: "Position with Heading"),
|
||||
subtitle: String(localized: "A previous position report showing the direction of travel.")
|
||||
)
|
||||
} header: {
|
||||
Text("Position History")
|
||||
}
|
||||
}
|
||||
|
||||
private var routeSection: some View {
|
||||
Section {
|
||||
MapLegendItem(
|
||||
symbol: AnyView(routeStartSymbol),
|
||||
title: String(localized: "Route Start"),
|
||||
subtitle: nil
|
||||
)
|
||||
MapLegendItem(
|
||||
symbol: AnyView(routeEndSymbol),
|
||||
title: String(localized: "Route End"),
|
||||
subtitle: nil
|
||||
)
|
||||
MapLegendItem(
|
||||
symbol: AnyView(routeLineSymbol),
|
||||
title: String(localized: "Route Line"),
|
||||
subtitle: String(localized: "Dashed line showing a recorded route path.")
|
||||
)
|
||||
} header: {
|
||||
Text("Routes")
|
||||
}
|
||||
}
|
||||
|
||||
private var convexHullSection: some View {
|
||||
Section {
|
||||
MapLegendItem(
|
||||
symbol: AnyView(convexHullSymbol),
|
||||
title: String(localized: "Convex Hull"),
|
||||
subtitle: String(localized: "An outline enclosing all LoRa node positions on the mesh.")
|
||||
)
|
||||
} header: {
|
||||
Text("Mesh Coverage")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Symbols
|
||||
|
||||
private var onlineNodeSymbol: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.green.opacity(0.3))
|
||||
.frame(width: 38, height: 38)
|
||||
CircleText(text: "ON", color: .green, circleSize: 28)
|
||||
}
|
||||
}
|
||||
|
||||
private var offlineNodeSymbol: some View {
|
||||
CircleText(text: "OFF", color: .gray, circleSize: 28)
|
||||
}
|
||||
|
||||
private var sensorNodeSymbol: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.blue)
|
||||
.frame(width: 28, height: 28)
|
||||
Image(systemName: "sensor.fill")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
private var waypointSymbol: some View {
|
||||
CircleText(text: "📍", color: .orange, circleSize: 28)
|
||||
}
|
||||
|
||||
private var precisionCircleSymbol: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.blue.opacity(0.25))
|
||||
.frame(width: 36, height: 36)
|
||||
Circle()
|
||||
.strokeBorder(Color.white, lineWidth: 1)
|
||||
.frame(width: 36, height: 36)
|
||||
Circle()
|
||||
.fill(Color.blue)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
|
||||
private var historyPointSymbol: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.blue)
|
||||
.frame(width: 12, height: 12)
|
||||
Circle()
|
||||
.stroke(Color.primary, lineWidth: 1)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
}
|
||||
|
||||
private var historyArrowSymbol: some View {
|
||||
Image(systemName: "location.north.circle.fill")
|
||||
.font(.system(size: 20))
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
|
||||
private var routeStartSymbol: some View {
|
||||
Circle()
|
||||
.fill(Color.green)
|
||||
.strokeBorder(Color.white, lineWidth: 2)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
|
||||
private var routeEndSymbol: some View {
|
||||
Circle()
|
||||
.fill(Color.black)
|
||||
.strokeBorder(Color.white, lineWidth: 2)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
|
||||
private var routeLineSymbol: some View {
|
||||
ZStack {
|
||||
Path { path in
|
||||
path.move(to: CGPoint(x: 4, y: 20))
|
||||
path.addLine(to: CGPoint(x: 36, y: 20))
|
||||
}
|
||||
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, dash: [6, 6]))
|
||||
.foregroundStyle(Color.blue)
|
||||
}
|
||||
}
|
||||
|
||||
private var convexHullSymbol: some View {
|
||||
ZStack {
|
||||
// Draw a simplified polygon shape
|
||||
ConvexHullShape()
|
||||
.fill(Color.indigo.opacity(0.4))
|
||||
ConvexHullShape()
|
||||
.stroke(Color.blue, lineWidth: 2)
|
||||
}
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ConvexHullShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
let w = rect.width
|
||||
let h = rect.height
|
||||
path.move(to: CGPoint(x: w * 0.5, y: h * 0.1))
|
||||
path.addLine(to: CGPoint(x: w * 0.85, y: h * 0.3))
|
||||
path.addLine(to: CGPoint(x: w * 0.9, y: h * 0.7))
|
||||
path.addLine(to: CGPoint(x: w * 0.6, y: h * 0.9))
|
||||
path.addLine(to: CGPoint(x: w * 0.15, y: h * 0.8))
|
||||
path.addLine(to: CGPoint(x: w * 0.1, y: h * 0.35))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ struct NodeMapSwiftUI: View {
|
|||
@State var isLookingAround = false
|
||||
@State var isShowingAltitude = false
|
||||
@State var isEditingSettings = false
|
||||
@State var isShowingLegend = false
|
||||
@State var isMeshMap = false
|
||||
@State var enabledOverlayConfigs: Set<UUID> = Set()
|
||||
|
||||
|
|
@ -97,6 +98,13 @@ struct NodeMapSwiftUI: View {
|
|||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
|
||||
}
|
||||
.sheet(isPresented: $isShowingLegend) {
|
||||
MapLegend(isMeshMap: false)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationContentInteraction(.scrolls)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
|
||||
}
|
||||
.onChange(of: selectedMapLayer) { _, newMapLayer in
|
||||
updateMapStyle(for: newMapLayer)
|
||||
}
|
||||
|
|
@ -168,17 +176,25 @@ struct NodeMapSwiftUI: View {
|
|||
|
||||
private var controlButtons: some View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isShowingLegend = !isShowingLegend
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isShowingLegend ? "map.fill" : "map")
|
||||
}
|
||||
.accessibilityLabel(isShowingLegend ? Text("Hide legend") : Text("Show legend"))
|
||||
.accessibilityHint(Text("Toggles the map legend"))
|
||||
.glassButtonStyle()
|
||||
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isEditingSettings = !isEditingSettings
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassButtonStyle()
|
||||
|
||||
if scene != nil {
|
||||
Button(action: {
|
||||
|
|
@ -188,11 +204,8 @@ struct NodeMapSwiftUI: View {
|
|||
isLookingAround = !isLookingAround
|
||||
}) {
|
||||
Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassButtonStyle()
|
||||
}
|
||||
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
|
|
@ -203,11 +216,8 @@ struct NodeMapSwiftUI: View {
|
|||
isShowingAltitude = !isShowingAltitude
|
||||
}) {
|
||||
Image(systemName: isShowingAltitude ? "mountain.2.fill" : "mountain.2")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassButtonStyle()
|
||||
}
|
||||
}
|
||||
.controlSize(.regular)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ struct MeshMap: View {
|
|||
@State var selectedWaypointId: String?
|
||||
@State var newWaypointCoord: CLLocationCoordinate2D?
|
||||
@State var isMeshMap = true
|
||||
@State private var showLegend = false
|
||||
/// Filter
|
||||
@StateObject var filters = NodeFilterParameters()
|
||||
|
||||
|
|
@ -158,20 +159,34 @@ struct MeshMap: View {
|
|||
filters: filters
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showLegend) {
|
||||
MapLegend(isMeshMap: true)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationContentInteraction(.scrolls)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showLegend = !showLegend
|
||||
}
|
||||
}) {
|
||||
Image(systemName: showLegend ? "map.fill" : "map")
|
||||
}
|
||||
.accessibilityLabel(showLegend ? "Hide map legend" : "Show map legend")
|
||||
.accessibilityHint(showLegend ? "Hides the map legend." : "Shows the map legend.")
|
||||
.glassButtonStyle()
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
editingSettings = !editingSettings
|
||||
}
|
||||
}) {
|
||||
Image(systemName: editingSettings ? "info.circle.fill" : "info.circle")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.glassButtonStyle()
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ struct RouteRecorder: View {
|
|||
.symbolRenderingMode(.multicolor)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.foregroundColor(.red)
|
||||
.glassButtonStyle()
|
||||
.buttonBorderShape(.circle)
|
||||
.matchedGeometryEffect(id: "Details Button", in: namespace)
|
||||
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ struct Routes: View {
|
|||
} label: {
|
||||
Label("Export", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.glassButtonStyle()
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue