Meshtastic-Apple/Meshtastic/Views/Nodes/MeshMap.swift
Garth Vander Houwen 3eef38926f
2.7.7 Working Changes (#1551)
* Bump version

* update the translations (#1540)

update the translations

* Don't alert (with sound: .default) when updating Live Activity (#1536)

* Fix adding channels (#1532)

* Full translation into Spanish (#1529)

* tapback with any emoji (#1538)

* Call clearStaleNodes at start of sendWantConfig (#1535)

* NFC Tag contact (#1537)

* Accessorymanager background discovery (#1542)

* Don't add new BLE  devices to the device list in the backgournd

* Bump version

* Update Meshtastic/MeshtasticApp.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Meshtastic/MeshtasticApp.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert "Full translation into Spanish (#1529)" (#1543)

This reverts commit f25fdfb89f.

* Revert "update the translations (#1540)" (#1544)

This reverts commit cb2fd8cc15.

* Revert "NFC Tag contact (#1537)" (#1545)

This reverts commit 5c22b8b6e0.

* Update Meshtastic/Views/Messages/TapbackInputView.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Meshtastic/Helpers/EmojiOnlyTextField.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert "Accessorymanager background discovery (#1542)" (#1553)

This reverts commit 487f24b99a.

* Update protobufs

* Remove UI Kit code, clean up waypoint form emoji picker

* Remove redundant nested Task in tapback emoji handler (#1552)

* Initial plan

* Remove nested Task block in tapback handler

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Delete empty file

* Handle nil for emoji keyboard type extension

* Remove UI kit method from waypoint form emoji picker

* Remove UI kit emoji picker from tapback

* Add Exchange User Info (#1550)

* Emoji keyboard (#1559)

* Add file missing from project, must have merged badly

* Remove ui kit emoji keyboard

* Discovery background fixes (#1561)

* Make BLE Transport an actor to fix background discovery crashes

* Protobufs

* Update Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Throw too many retries error again, remove return

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Increase connection timeout

* Update protobufs

* Revert "Fix adding channels (#1532)" (#1562)

This reverts commit bff8ca018b.

---------

Co-authored-by: MGJ <62177301+MGJ520@users.noreply.github.com>
Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu>
Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Co-authored-by: Alvaro Samudio <alvarosamudio@protonmail.com>
Co-authored-by: Mathew Kamkar <578302+matkam@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
Co-authored-by: Brian Hardie <777730+bhardie@users.noreply.github.com>
2026-01-15 14:13:40 -08:00

220 lines
8.1 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
//  MeshMap.swift
//  Meshtastic
//
//  Copyright(c) Garth Vander Houwen 11/7/23.
//
import SwiftUI
import CoreData
import CoreLocation
import Foundation
import OSLog
import MapKit
struct MeshMap: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var accessoryManager: AccessoryManager
@ObservedObject
var router: Router
/// Parameters
@State var showUserLocation: Bool = true
/// Map State User Defaults
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .standard
/// Map overlay configs
@State private var enabledOverlayConfigs: Set<UUID> = []
// Map Configuration
@Namespace var mapScope
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .excludingAll, showsTraffic: false)
@State var position = MapCameraPosition.automatic
@State private var distance = 10000.0
@State private var editingSettings = false
@State private var editingFilters = false
@State var selectedPosition: PositionEntity?
@State var editingWaypoint: WaypointEntity?
@State var selectedWaypoint: WaypointEntity?
@State var selectedWaypointId: String?
@State var newWaypointCoord: CLLocationCoordinate2D?
@State var isMeshMap = true
/// Filter
@StateObject var filters = NodeFilterParameters()
var body: some View {
NavigationStack {
ZStack {
MapReader { reader in
Map(
position: $position,
bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity),
scope: mapScope
) {
MeshMapContent(
showUserLocation: $showUserLocation,
showTraffic: $showTraffic,
showPointsOfInterest: $showPointsOfInterest,
selectedMapLayer: $selectedMapLayer,
selectedPosition: $selectedPosition,
selectedWaypoint: $selectedWaypoint,
enabledOverlayConfigs: $enabledOverlayConfigs
)
}
.mapScope(mapScope)
.mapStyle(mapStyle)
.mapControls {
MapScaleView(scope: mapScope)
.mapControlVisibility(.automatic)
MapPitchToggle(scope: mapScope)
.mapControlVisibility(.automatic)
MapCompass(scope: mapScope)
.mapControlVisibility(.automatic)
}
.controlSize(.regular)
.offset(y: 100)
.onMapCameraChange(frequency: MapCameraUpdateFrequency.onEnd, { context in
// distance is only used for long-press waypoint creation, so we don't need continuous updates which touch @State and force rerenders as we pan and (for distance in particular) zoom around the map. onEnd is more than enough.
distance = context.camera.distance
})
.onTapGesture(count: 1, perform: { position in
newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init()
})
.gesture(
LongPressGesture(minimumDuration: 0.5)
.sequenced(before: SpatialTapGesture(coordinateSpace: .local))
.onEnded { value in
switch value {
case let .second(_, tapValue):
guard let point = tapValue?.location else {
Logger.services.error("Unable to retreive tap location from gesture data.")
return
}
guard let coordinate = reader.convert(point, from: .local) else {
Logger.services.error("Unable to convert local point to coordinate on map.")
return
}
centerMapAt(coordinate: coordinate)
newWaypointCoord = coordinate
editingWaypoint = WaypointEntity(context: context)
editingWaypoint!.name = "Waypoint Pin"
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7)
editingWaypoint!.longitudeI = Int32((newWaypointCoord?.longitude ?? 0) * 1e7)
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
editingWaypoint!.id = 0
Logger.services.debug("Long press occured at Lat: \(coordinate.latitude, privacy: .public) Long: \(coordinate.longitude, privacy: .public)")
default: return
}
}
)
.ignoresSafeArea()
}
.sheet(item: $selectedPosition) { selection in
PositionPopover(position: selection, popover: false)
.padding()
}
.sheet(item: $selectedWaypoint) { selection in
WaypointForm(waypoint: selection)
.presentationDetents([.large])
}
.sheet(item: $editingWaypoint) { selection in
WaypointForm(waypoint: selection, editMode: true)
.presentationDetents([.large])
}
.sheet(isPresented: $editingSettings) {
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
}
.onChange(of: router.navigationState) {
guard case .map = router.navigationState.selectedTab else { return }
// TODO: handle deep link for waypoints
}
.onChange(of: selectedMapLayer) { _, newMapLayer in
switch selectedMapLayer {
case .standard:
UserDefaults.mapLayer = newMapLayer
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
case .hybrid:
UserDefaults.mapLayer = newMapLayer
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
case .satellite:
UserDefaults.mapLayer = newMapLayer
mapStyle = MapStyle.imagery(elevation: .realistic)
case .offline:
return
}
}
.sheet(isPresented: $editingFilters) {
NodeListFilter(
filters: filters
)
}
.safeAreaInset(edge: .bottom, alignment: .trailing) {
HStack {
Spacer()
Button(action: {
withAnimation {
editingSettings = !editingSettings
}
}) {
Image(systemName: editingSettings ? "info.circle.fill" : "info.circle")
.padding(.vertical, 5)
}
.tint(Color(UIColor.secondarySystemBackground))
.foregroundColor(.accentColor)
.buttonStyle(.borderedProminent)
}
.controlSize(.regular)
.padding(5)
}
}
.navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack {
ConnectedDevice(deviceConnected: accessoryManager.isConnected, name: accessoryManager.activeConnection?.device.shortName ?? "?")
})
.toolbarBackground(.hidden, for: .navigationBar)
}
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
// Initialize enabled overlay configs with all active files
let activeFiles = GeoJSONOverlayManager.shared.getUploadedFilesWithState().filter { $0.isActive }
enabledOverlayConfigs = Set(activeFiles.map { $0.id })
switch selectedMapLayer {
case .standard:
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
case .hybrid:
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
case .satellite:
mapStyle = MapStyle.imagery(elevation: .realistic)
case .offline:
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
}
}
.onDisappear(perform: {
UIApplication.shared.isIdleTimerDisabled = false
})
.onReceive(NotificationCenter.default.publisher(for: Foundation.Notification.Name.mapDataFileDeleted)) { notification in
if let deletedFileId = notification.object as? UUID {
enabledOverlayConfigs.remove(deletedFileId)
}
}
}
// moves the map to a new coordinate
private func centerMapAt(coordinate: CLLocationCoordinate2D) {
withAnimation(.easeInOut(duration: 0.2), {
position = .camera(
MapCamera(
centerCoordinate: coordinate, // Set new center
distance: distance, // Preserve current zoom distance
heading: 0, // align north
pitch: 0 // set view to top down
)
)
})
}
}