mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
cleanup
This commit is contained in:
parent
1e95694c7b
commit
ee43a3f4dc
5 changed files with 213 additions and 274 deletions
|
|
@ -59,7 +59,6 @@
|
|||
3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; };
|
||||
3D3417C82E29D38A006A988B /* GeoJSONOverlayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */; };
|
||||
3D3417CB2E29D3B0006A988B /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417C92E29D3B0006A988B /* Color+Hex.swift */; };
|
||||
3D3417CC2E29D3B0006A988B /* Data+Gzip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417CA2E29D3B0006A988B /* Data+Gzip.swift */; };
|
||||
3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417D12E2DC260006A988B /* MapDataManager.swift */; };
|
||||
3D3417D42E2DC293006A988B /* MapDataUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417D32E2DC293006A988B /* MapDataUpload.swift */; };
|
||||
6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D825E612C34786C008DBEE4 /* CommonRegex.swift */; };
|
||||
|
|
@ -335,7 +334,6 @@
|
|||
3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = "<group>"; };
|
||||
3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayConfig.swift; sourceTree = "<group>"; };
|
||||
3D3417C92E29D3B0006A988B /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = "<group>"; };
|
||||
3D3417CA2E29D3B0006A988B /* Data+Gzip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Gzip.swift"; sourceTree = "<group>"; };
|
||||
3D3417D12E2DC260006A988B /* MapDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDataManager.swift; sourceTree = "<group>"; };
|
||||
3D3417D32E2DC293006A988B /* MapDataUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDataUpload.swift; sourceTree = "<group>"; };
|
||||
6D825E612C34786C008DBEE4 /* CommonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRegex.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -1183,7 +1181,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
3D3417C92E29D3B0006A988B /* Color+Hex.swift */,
|
||||
3D3417CA2E29D3B0006A988B /* Data+Gzip.swift */,
|
||||
DD007BB12AA59B9A00F5FA12 /* CoreData */,
|
||||
DDFFA7462B3A7F3C004730DB /* Bundle.swift */,
|
||||
DDDB444529F8A96500EE2349 /* Character.swift */,
|
||||
|
|
@ -1595,7 +1592,6 @@
|
|||
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */,
|
||||
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
|
||||
3D3417CB2E29D3B0006A988B /* Color+Hex.swift in Sources */,
|
||||
3D3417CC2E29D3B0006A988B /* Data+Gzip.swift in Sources */,
|
||||
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
|
||||
BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */,
|
||||
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
import Foundation
|
||||
import Compression
|
||||
|
||||
extension Data {
|
||||
/// Decompresses raw deflate data
|
||||
func zlibDecompressed() throws -> Data {
|
||||
guard self.count > 0 else { return Data() }
|
||||
|
||||
// Try Foundation's zlib first
|
||||
do {
|
||||
let decompressedData = try (self as NSData).decompressed(using: .zlib) as Data
|
||||
print("Data+Zlib: Successfully decompressed with Foundation \(count) bytes to \(decompressedData.count) bytes")
|
||||
return decompressedData
|
||||
} catch {
|
||||
print("Data+Zlib: Foundation decompression failed: \(error), trying raw deflate...")
|
||||
}
|
||||
|
||||
// Fallback to Compression framework with raw deflate
|
||||
let bufferSize = count * 10
|
||||
let destination = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
defer { destination.deallocate() }
|
||||
|
||||
return try self.withUnsafeBytes { bytes in
|
||||
let source = bytes.bindMemory(to: UInt8.self)
|
||||
|
||||
let result = compression_decode_buffer(
|
||||
destination, bufferSize,
|
||||
source.baseAddress!, count,
|
||||
nil, COMPRESSION_ZLIB
|
||||
)
|
||||
|
||||
guard result > 0 else {
|
||||
print("Data+Zlib: Raw deflate decompression also failed, result size: \(result)")
|
||||
throw ZlibError.decompression
|
||||
}
|
||||
|
||||
print("Data+Zlib: Successfully decompressed with raw deflate \(count) bytes to \(result) bytes")
|
||||
return Data(bytes: destination, count: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ZlibError: Error {
|
||||
case decompression
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .decompression:
|
||||
return "Failed to decompress data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GzipError: Error {
|
||||
case decompression
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .decompression:
|
||||
return "Failed to decompress gzip data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ class MapDataManager {
|
|||
}
|
||||
|
||||
// Check file extension
|
||||
let allowedExtensions = ["json", "geojson", "kml", "kmz", "gz", "zlib"]
|
||||
let allowedExtensions = ["json", "geojson"]
|
||||
let fileExtension = url.pathExtension.lowercased()
|
||||
guard allowedExtensions.contains(fileExtension) else {
|
||||
throw MapDataError.unsupportedFormat
|
||||
|
|
@ -133,15 +133,22 @@ class MapDataManager {
|
|||
Task.detached {
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let processedData = try self.processData(data, filename: url.lastPathComponent)
|
||||
let overlayCount = try self.getOverlayCount(from: processedData)
|
||||
continuation.resume(returning: (processedData, overlayCount))
|
||||
let overlayCount = try self.getOverlayCount(from: data)
|
||||
continuation.resume(returning: (data, overlayCount))
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add proper GeoJSON schema validation here
|
||||
// - Validate required properties (type, features)
|
||||
// - Validate geometry types and coordinates
|
||||
// - Validate feature structure
|
||||
// - Consider using JSONSchema validation
|
||||
// - Ensure coordinates are within valid ranges (lat: -90 to 90, lon: -180 to 180)
|
||||
// - Validate that feature properties follow expected patterns
|
||||
|
||||
// If this is the first file uploaded, make it active by default
|
||||
let isFirstFile = uploadedFiles.isEmpty
|
||||
|
||||
|
|
@ -158,17 +165,6 @@ class MapDataManager {
|
|||
)
|
||||
}
|
||||
|
||||
/// Process data (decompress if needed)
|
||||
private func processData(_ data: Data, filename: String) throws -> Data {
|
||||
let fileExtension = filename.components(separatedBy: ".").last?.lowercased() ?? ""
|
||||
|
||||
switch fileExtension {
|
||||
case "gz", "zlib":
|
||||
return try data.zlibDecompressed()
|
||||
default:
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
/// Get overlay count from raw GeoJSON data
|
||||
private func getOverlayCount(from data: Data) throws -> Int {
|
||||
|
|
@ -180,6 +176,16 @@ class MapDataManager {
|
|||
throw MapDataError.invalidContent
|
||||
}
|
||||
|
||||
/// Load feature collection from a single file
|
||||
private func loadFeatureCollectionFromFile(_ file: MapDataMetadata) throws -> GeoJSONFeatureCollection? {
|
||||
guard let fileURL = getUserUploadedDirectory()?.appendingPathComponent(file.filename) else {
|
||||
throw MapDataError.fileNotFound
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return try JSONDecoder().decode(GeoJSONFeatureCollection.self, from: data)
|
||||
}
|
||||
|
||||
// MARK: - Configuration Loading
|
||||
|
||||
/// Load combined feature collection from specific files
|
||||
|
|
@ -248,8 +254,7 @@ class MapDataManager {
|
|||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let processedData = try processData(data, filename: activeFile.filename)
|
||||
let featureCollection = try JSONDecoder().decode(GeoJSONFeatureCollection.self, from: processedData)
|
||||
let featureCollection = try JSONDecoder().decode(GeoJSONFeatureCollection.self, from: data)
|
||||
|
||||
allFeatures.append(contentsOf: featureCollection.features)
|
||||
} catch {
|
||||
|
|
@ -291,7 +296,7 @@ class MapDataManager {
|
|||
}
|
||||
|
||||
/// Delete uploaded file
|
||||
func deleteFile(_ metadata: MapDataMetadata) throws {
|
||||
func deleteFile(_ metadata: MapDataMetadata) async throws {
|
||||
|
||||
guard let fileURL = getUserUploadedDirectory()?.appendingPathComponent(metadata.filename) else {
|
||||
Logger.services.error("🗑️ MapDataManager: Could not construct file URL for: \(metadata.filename, privacy: .public)")
|
||||
|
|
@ -334,27 +339,6 @@ class MapDataManager {
|
|||
|
||||
}
|
||||
|
||||
/// Toggle file active status
|
||||
func toggleFileActive(_ metadata: MapDataMetadata) throws {
|
||||
if let index = uploadedFiles.firstIndex(where: { $0.filename == metadata.filename }) {
|
||||
let newActiveState = !uploadedFiles[index].isActive
|
||||
|
||||
// If making this file active, deactivate all others (only one can be active)
|
||||
if newActiveState {
|
||||
for i in uploadedFiles.indices {
|
||||
uploadedFiles[i].isActive = (i == index)
|
||||
}
|
||||
} else {
|
||||
// Just deactivate this file
|
||||
uploadedFiles[index].isActive = false
|
||||
}
|
||||
|
||||
try saveMetadata()
|
||||
|
||||
// Clear cache to force reload
|
||||
activeFeatureCollection = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Metadata Persistence
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ struct NodeMapSwiftUI: View {
|
|||
@State var isShowingAltitude = false
|
||||
@State var isEditingSettings = false
|
||||
@State var isMeshMap = false
|
||||
@State var enabledOverlayConfigs: Set<UUID> = Set()
|
||||
|
||||
@State private var mapRegion = MKCoordinateRegion.init()
|
||||
|
||||
|
|
@ -40,165 +41,191 @@ struct NodeMapSwiftUI: View {
|
|||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
var body: some View {
|
||||
var mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
|
||||
if node.hasPositions {
|
||||
mapWithNavigation
|
||||
} else {
|
||||
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
|
||||
}
|
||||
}
|
||||
|
||||
private var mapWithNavigation: some View {
|
||||
ZStack {
|
||||
MapReader { _ in
|
||||
configuredMap
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
MapReader { _ in
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 0, maximumDistance: .infinity), scope: mapScope) {
|
||||
NodeMapContent(node: node)
|
||||
}
|
||||
.mapScope(mapScope)
|
||||
.mapStyle(mapStyle)
|
||||
.mapControls {
|
||||
MapScaleView(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
if showUserLocation {
|
||||
MapUserLocationButton(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
}
|
||||
MapPitchToggle(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
MapCompass(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.overlay(alignment: .bottom) {
|
||||
if scene != nil && isLookingAround {
|
||||
LookAroundPreview(initialScene: scene)
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
if !isLookingAround && isShowingAltitude {
|
||||
PositionAltitudeChart(node: node)
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
.onChange(of: (selectedMapLayer)) { _, newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.imagery(elevation: .flat)
|
||||
case .offline:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: node) {
|
||||
isLookingAround = false
|
||||
isShowingAltitude = false
|
||||
mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
position = .automatic
|
||||
} else {
|
||||
position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: distance, heading: 0, pitch: 0))
|
||||
}
|
||||
if let mostRecent {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mostRecent.coordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
mapStyle = MapStyle.imagery(elevation: .flat)
|
||||
case .offline:
|
||||
mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
}
|
||||
mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
position = .automatic
|
||||
} else {
|
||||
if let mrCoord = mostRecent?.coordinate {
|
||||
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0))
|
||||
}
|
||||
}
|
||||
if self.scene == nil {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mostRecent!.coordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isEditingSettings = !isEditingSettings
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
/// Look Around Button
|
||||
if self.scene != nil {
|
||||
Button(action: {
|
||||
if isShowingAltitude {
|
||||
isShowingAltitude = false
|
||||
}
|
||||
isLookingAround = !isLookingAround
|
||||
}) {
|
||||
Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
/// Altitude Button
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
Button(action: {
|
||||
if isLookingAround {
|
||||
isLookingAround = false
|
||||
}
|
||||
isShowingAltitude = !isShowingAltitude
|
||||
}) {
|
||||
Image(systemName: isShowingAltitude ? "mountain.2.fill" : "mountain.2")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.onDisappear {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}}
|
||||
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
})
|
||||
} else {
|
||||
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
|
||||
}
|
||||
|
||||
private var configuredMap: some View {
|
||||
baseMap
|
||||
.overlay(alignment: .bottom) {
|
||||
lookAroundView
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
altitudeView
|
||||
}
|
||||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
|
||||
}
|
||||
.onChange(of: selectedMapLayer) { _, newMapLayer in
|
||||
updateMapStyle(for: newMapLayer)
|
||||
}
|
||||
.onChange(of: node) {
|
||||
handleNodeChange()
|
||||
}
|
||||
.onAppear {
|
||||
handleAppear()
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
controlButtons
|
||||
}
|
||||
.onDisappear {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private var baseMap: some View {
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 0, maximumDistance: .infinity), scope: mapScope) {
|
||||
NodeMapContent(node: node)
|
||||
}
|
||||
.mapScope(mapScope)
|
||||
.mapStyle(mapStyle)
|
||||
.mapControls {
|
||||
MapScaleView(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
if showUserLocation {
|
||||
MapUserLocationButton(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
}
|
||||
MapPitchToggle(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
MapCompass(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
}
|
||||
|
||||
private var lookAroundView: some View {
|
||||
Group {
|
||||
if scene != nil && isLookingAround {
|
||||
LookAroundPreview(initialScene: scene)
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var altitudeView: some View {
|
||||
Group {
|
||||
if !isLookingAround && isShowingAltitude {
|
||||
PositionAltitudeChart(node: node)
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var controlButtons: some View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isEditingSettings = !isEditingSettings
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
if scene != nil {
|
||||
Button(action: {
|
||||
if isShowingAltitude {
|
||||
isShowingAltitude = false
|
||||
}
|
||||
isLookingAround = !isLookingAround
|
||||
}) {
|
||||
Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
Button(action: {
|
||||
if isLookingAround {
|
||||
isLookingAround = false
|
||||
}
|
||||
isShowingAltitude = !isShowingAltitude
|
||||
}) {
|
||||
Image(systemName: isShowingAltitude ? "mountain.2.fill" : "mountain.2")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
|
||||
private func updateMapStyle(for layer: MapLayer) {
|
||||
UserDefaults.mapLayer = layer
|
||||
switch layer {
|
||||
case .standard:
|
||||
mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
mapStyle = MapStyle.imagery(elevation: .flat)
|
||||
case .offline:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func handleNodeChange() {
|
||||
isLookingAround = false
|
||||
isShowingAltitude = false
|
||||
let newMostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
position = .automatic
|
||||
} else if let mrCoord = newMostRecent?.coordinate {
|
||||
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0))
|
||||
}
|
||||
if let newMostRecent {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: newMostRecent.coordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleAppear() {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
updateMapStyle(for: selectedMapLayer)
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
position = .automatic
|
||||
} else if let mrCoord = mostRecent?.coordinate {
|
||||
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0))
|
||||
}
|
||||
if scene == nil, let mrCoord = mostRecent?.coordinate {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mrCoord)
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Get the look around scene
|
||||
|
|
|
|||
|
|
@ -106,11 +106,7 @@ struct MapDataUpload: View {
|
|||
isPresented: $isShowingFilePicker,
|
||||
allowedContentTypes: [
|
||||
UTType.json,
|
||||
UTType(filenameExtension: "geojson") ?? UTType.json,
|
||||
UTType(filenameExtension: "kml") ?? UTType.xml,
|
||||
UTType(filenameExtension: "kmz") ?? UTType.zip,
|
||||
UTType(filenameExtension: "gz") ?? UTType.data,
|
||||
UTType(filenameExtension: "zlib") ?? UTType.data
|
||||
UTType(filenameExtension: "geojson") ?? UTType.json
|
||||
],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
|
|
@ -183,20 +179,19 @@ struct MapDataUpload: View {
|
|||
}
|
||||
|
||||
private func toggleFileActive(_ file: MapDataMetadata) {
|
||||
do {
|
||||
try mapDataManager.toggleFileActive(file)
|
||||
} catch {
|
||||
errorMessage = "Failed to toggle file: \(error.localizedDescription)"
|
||||
showError = true
|
||||
}
|
||||
mapDataManager.toggleFileActive(file.id)
|
||||
}
|
||||
|
||||
private func deleteFile(_ file: MapDataMetadata) {
|
||||
do {
|
||||
try mapDataManager.deleteFile(file)
|
||||
} catch {
|
||||
errorMessage = "Failed to delete file: \(error.localizedDescription)"
|
||||
showError = true
|
||||
Task {
|
||||
do {
|
||||
try await mapDataManager.deleteFile(file)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
errorMessage = "Failed to delete file: \(error.localizedDescription)"
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue