Merge pull request #357 from meshtastic/offline_maps_updates

Offline maps updates
This commit is contained in:
Garth Vander Houwen 2023-05-10 07:53:10 -07:00 committed by GitHub
commit 791c3d216b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 781 additions and 234 deletions

View file

@ -30,7 +30,7 @@
DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD415827285859C4009B0E59 /* TelemetryConfig.swift */; };
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41582928585C32009B0E59 /* RangeTestConfig.swift */; };
DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */; };
DD457188293C7E63000C49FB /* SignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */; };
DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */; };
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; };
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; };
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; };
@ -98,6 +98,13 @@
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABE128B13FB500384BA1 /* PositionConfigEnums.swift */; };
DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */; };
DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABE528B1406100384BA1 /* LoraConfigEnums.swift */; };
DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A0E2A05920E006ED576 /* FileManager.swift */; };
DDB75A112A059258006ED576 /* Url.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A102A059258006ED576 /* Url.swift */; };
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; };
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; };
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; };
DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1B2A076DFA006ED576 /* TilesView.swift */; };
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; };
DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; };
DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; };
DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; };
@ -208,7 +215,7 @@
DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeWeatherForecast.swift; sourceTree = "<group>"; };
DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalStrengthIndicator.swift; sourceTree = "<group>"; };
DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLESignalStrengthIndicator.swift; sourceTree = "<group>"; };
DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = "<group>"; };
DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = "<group>"; };
DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = "<group>"; };
@ -281,6 +288,13 @@
DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayEnums.swift; sourceTree = "<group>"; };
DDB6ABE528B1406100384BA1 /* LoraConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoraConfigEnums.swift; sourceTree = "<group>"; };
DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV12.xcdatamodel; sourceTree = "<group>"; };
DDB75A0E2A05920E006ED576 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
DDB75A102A059258006ED576 /* Url.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Url.swift; sourceTree = "<group>"; };
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = "<group>"; };
DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = "<group>"; };
DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = "<group>"; };
DDB75A1B2A076DFA006ED576 /* TilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesView.swift; sourceTree = "<group>"; };
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = "<group>"; };
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = "<group>"; };
DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; };
DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = "<group>"; };
@ -389,6 +403,7 @@
DD964FC32974767D007C176F /* MapViewFitExtension.swift */,
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
DDDB443529F6287000EE2349 /* MapButtons.swift */,
DDB75A1B2A076DFA006ED576 /* TilesView.swift */,
);
path = Custom;
sourceTree = "<group>";
@ -544,6 +559,15 @@
path = Protobufs;
sourceTree = "<group>";
};
DDB75A122A0593CD006ED576 /* Map */ = {
isa = PBXGroup;
children = (
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */,
DDB75A152A0594AD006ED576 /* TileOverlay.swift */,
);
path = Map;
sourceTree = "<group>";
};
DDC2E14B26CE248E0042C5E4 = {
isa = PBXGroup;
children = (
@ -641,6 +665,7 @@
DDC2E18926CE24F70042C5E4 /* Resources */ = {
isa = PBXGroup;
children = (
DDB75A192A05EB67006ED576 /* alpha.png */,
DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */,
);
path = Resources;
@ -669,7 +694,8 @@
DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */,
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */,
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */,
DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */,
DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */,
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -677,6 +703,7 @@
DDC2E1A526CEB32B0042C5E4 /* Helpers */ = {
isa = PBXGroup;
children = (
DDB75A122A0593CD006ED576 /* Map */,
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
@ -716,6 +743,8 @@
DDDB444729F8A9C900EE2349 /* String.swift */,
DDDB444F29F8AC9C00EE2349 /* UIImage.swift */,
DDDB443F29F79AB000EE2349 /* UserDefaults.swift */,
DDB75A0E2A05920E006ED576 /* FileManager.swift */,
DDB75A102A059258006ED576 /* Url.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -885,6 +914,7 @@
DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */,
DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */,
DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */,
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */,
DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -940,7 +970,7 @@
files = (
DDDB444829F8A9C900EE2349 /* String.swift in Sources */,
DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */,
DD457188293C7E63000C49FB /* SignalStrengthIndicator.swift in Sources */,
DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */,
DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */,
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */,
@ -955,6 +985,7 @@
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */,
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */,
DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */,
DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */,
DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */,
DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */,
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
@ -979,6 +1010,8 @@
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */,
DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */,
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */,
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */,
@ -1016,6 +1049,7 @@
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */,
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */,
DDB75A112A059258006ED576 /* Url.swift in Sources */,
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */,
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */,
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */,
@ -1025,6 +1059,8 @@
DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */,
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */,
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */,
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */,
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */,
@ -1265,7 +1301,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.8;
MARKETING_VERSION = 2.1.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1299,7 +1335,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.8;
MARKETING_VERSION = 2.1.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1418,7 +1454,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.8;
MARKETING_VERSION = 2.1.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1449,7 +1485,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.8;
MARKETING_VERSION = 2.1.9;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "alpha.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -128,47 +128,134 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
}
}
}
enum MapTileServerLinks: Int, CaseIterable, Identifiable {
enum MapLayer: String, CaseIterable, Equatable {
case standard
case hybrid
case satellite
case offline
var localized: String { self.rawValue.localized }
}
enum MapTileServerLinks: String, CaseIterable, Identifiable {
case none = 0
case openStreetMaps = 1
case wikimedia = 2
case nationalMap = 3
var id: Int { self.rawValue }
case openStreetMap
case openStreetMapDE
case openStreetMapFR
case openCycleMap
case openStreetMapHot
case openTopoMap
case usgsTopo
case usgsImageryTopo
case usgsImageryOnly
case toner
case watercolor
var id: String { self.rawValue }
var attribution: String {
switch self {
case .openStreetMap:
return "Map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)"
case .openStreetMapDE:
return "[OpenStreetMap DE](https://openstreetmap.de) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)"
case .openStreetMapFR:
return "[OpenStreetMap FR](https://www.openstreetmap.fr) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)"
case .openCycleMap:
return "[OpenCycleMap](https://www.cyclosm.org) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)"
case .openTopoMap:
return "[OpenTopoMap](https://opentopomap.org) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)"
case .openStreetMapHot:
return "[OpenStreetMap FR](https://www.openstreetmap.fr) map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)"
case .usgsTopo:
return "[USGS](https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer) [National Map](http://nationalmap.gov/) topographic overlay."
case .usgsImageryTopo:
return "[USGS](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer) [National Map](http://nationalmap.gov/) imagery and topographic overlay."
case .usgsImageryOnly:
return "[USGS](https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer) [National Map](http://nationalmap.gov/) imagery only overlay."
case .toner:
return "[Stamen Design's](https://github.com/stamen/toner-carto) black and white map tiles."
case .watercolor:
return "Cooper Hewitt, Smithsonian Design Museum's [Watercolor Maptiles](https://watercolormaps.collection.cooperhewitt.org/) is a open-source mapping tool created by Stamen Design and built on [OpenStreetMap](http://www.openstreetmap.org) data."
}
}
var description: String {
switch self {
case .none:
return "Please Select"
case .wikimedia:
return "Wikimedia"
case .openStreetMaps:
return "Open Street Maps"
case .nationalMap:
return "US National Map"
case .openStreetMap:
return "Open Street Map"
case .openStreetMapDE:
return "Open Street Map DE"
case .openStreetMapFR:
return "Open Street Map FR"
case .openCycleMap:
return "Open Cycle Map"
case .openStreetMapHot:
return "Humanitarian (OSM)"
case.openTopoMap:
return "Open Topo Map"
case .usgsTopo:
return "USGS Topographic"
case .usgsImageryTopo:
return "USGS Topo Imagery"
case .usgsImageryOnly:
return "USGS Imagery Only"
case .toner:
return "Toner"
case .watercolor:
return "Watercolor Maptiles"
}
}
var tileUrl: String {
switch self {
case .none:
return ""
case .wikimedia:
return "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png"
case .openStreetMaps:
case .openStreetMap:
return "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
case .nationalMap:
case .openStreetMapDE:
return "https://tile.openstreetmap.de/{z}/{x}/{y}.png"
case .openStreetMapFR:
return "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png"
case .openCycleMap:
return "https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png"
case .openStreetMapHot:
return "https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"
case .openTopoMap:
return "https://a.tile.opentopomap.org/{z}/{x}/{y}.png"
case .usgsTopo:
return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}"
case .usgsImageryTopo:
return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}"
case .usgsImageryOnly:
return "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}"
case .toner:
return "https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png"
case .watercolor:
return "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg"
}
}
var zoomRange: [Int] {
switch self {
case .none:
return [Int](0...1)
case .wikimedia:
return [Int](0...24)
case .openStreetMaps:
return [Int](0...24)
case .nationalMap:
return [Int](0...24)
case .openStreetMap:
return [Int](0...18)
case .openStreetMapDE:
return [Int](0...18)
case .openStreetMapFR:
return [Int](0...18)
case .openCycleMap:
return [Int](0...18)
case .openTopoMap:
return [Int](0...18)
case .openStreetMapHot:
return [Int](0...18)
case .usgsTopo:
return [Int](6...15)
case .usgsImageryTopo:
return [Int](6...15)
case .usgsImageryOnly:
return [Int](6...15)
case .toner:
return [Int](0...18)
case .watercolor:
return [Int](0...18)
}
}
}

View file

@ -54,6 +54,7 @@ enum SenderIntervals: Int, CaseIterable, Identifiable {
case off = 0
case fifteenSeconds = 15
case thirtySeconds = 30
case fortyFiveSeconds = 45
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
@ -70,6 +71,8 @@ enum SenderIntervals: Int, CaseIterable, Identifiable {
return "interval.fifteen.seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
case .fortyFiveSeconds:
return "interval.fortyfive.seconds".localized
case .oneMinute:
return "interval.one.minute".localized
case .fiveMinutes:
@ -91,6 +94,7 @@ enum UpdateIntervals: Int, CaseIterable, Identifiable {
case tenSeconds = 10
case fifteenSeconds = 15
case thirtySeconds = 30
case fortyFiveSeconds = 45
case oneMinute = 60
case twoMinutes = 120
case fiveMinutes = 300
@ -120,6 +124,8 @@ enum UpdateIntervals: Int, CaseIterable, Identifiable {
return "interval.fifteen.seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
case .fortyFiveSeconds:
return "interval.fortyfive.seconds".localized
case .oneMinute:
return "interval.one.minute".localized
case .twoMinutes:

View file

@ -0,0 +1,65 @@
//
// FileManager.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 5/5/23.
//
import Foundation
let allocatedSizeResourceKeys: Set<URLResourceKey> = [
.isRegularFileKey,
.fileAllocatedSizeKey,
.totalFileAllocatedSizeKey,
]
public extension FileManager {
/// Calculate the allocated size of a directory and all its contents on the volume.
///
/// As there's no simple way to get this information from the file system the method
/// has to crawl the entire hierarchy, accumulating the overall sum on the way.
/// The resulting value is roughly equivalent with the amount of bytes
/// that would become available on the volume if the directory would be deleted.
///
/// - note: There are a couple of oddities that are not taken into account (like symbolic links, meta data of
/// directories, hard links, ...).
func allocatedSizeOfDirectory(at directoryURL: URL) -> String {
// The error handler simply stores the error and stops traversal
var enumeratorError: Error? = nil
func errorHandler(_: URL, error: Error) -> Bool {
enumeratorError = error
return false
}
// We have to enumerate all directory contents, including subdirectories.
let enumerator = self.enumerator(at: directoryURL,
includingPropertiesForKeys: Array(allocatedSizeResourceKeys),
options: [],
errorHandler: errorHandler)!
// We'll sum up content size here:
var accumulatedSize: UInt64 = 0
// Perform the traversal.
for item in enumerator {
// Bail out on errors from the errorHandler.
if enumeratorError != nil { break }
// Add up individual file sizes.
guard let contentItemURL = item as? URL else { continue }
do {
accumulatedSize += try contentItemURL.regularFileAllocatedSize()
} catch {
print("❤️ \(error.localizedDescription)")
}
}
if let error = enumeratorError { print("❤️ AllocatedSizeOfDirectory enumeratorError = \(error.localizedDescription)") }
return Double(accumulatedSize).toBytes
}
}

View file

@ -30,6 +30,18 @@ extension String {
var localized: String { NSLocalizedString(self, comment: self) }
func isEmoji() -> Bool {
// Emoji are no more than 4 bytes
if self.count > 4 {
return false
} else {
let characters = Array(self)
return characters[0].isEmoji
}
}
func onlyEmojis() -> Bool {
return count > 0 && !contains { !$0.isEmoji }
}

View file

@ -0,0 +1,21 @@
//
// Url.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 5/5/23.
//
import Foundation
extension URL {
func regularFileAllocatedSize() throws -> UInt64 {
let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)
guard resourceValues.isRegularFile ?? false else {
return 0
}
return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0)
}
}

View file

@ -10,33 +10,23 @@ import Foundation
extension UserDefaults {
enum Keys: String, CaseIterable {
case hasBeenLaunched
case meshtasticUsername
case preferredPeripheralId
case provideLocation
case provideLocationInterval
case meshMapType
case meshMapCenteringMode
case mapLayer
case meshMapRecentering
case meshMapCustomTileServer
case meshMapShowNodeHistory
case meshMapShowRouteLines
case enableOfflineMaps
case mapTileServer
case mapTilesAboveLabels
}
func reset() {
Keys.allCases.forEach { removeObject(forKey: $0.rawValue) }
}
static var hasBeenLaunched: Bool {
get {
let result = UserDefaults.standard.bool(forKey: "hasBeenLaunched")
UserDefaults.standard.set(true, forKey: "hasBeenLaunched")
return result
} set {
UserDefaults.standard.set(newValue, forKey: "hasBeenLaunched")
}
}
static var meshtasticUsername: String {
get {
UserDefaults.standard.string(forKey: "meshtasticUsername") ?? ""
@ -57,9 +47,7 @@ extension UserDefaults {
static var provideLocation: Bool {
get {
let result = UserDefaults.standard.bool(forKey: "provideLocation")
UserDefaults.standard.set(true, forKey: "provideLocation")
return result
UserDefaults.standard.bool(forKey: "provideLocation")
} set {
UserDefaults.standard.set(newValue, forKey: "provideLocation")
}
@ -74,12 +62,12 @@ extension UserDefaults {
}
}
static var mapType: Int {
static var mapLayer: MapLayer {
get {
UserDefaults.standard.integer(forKey: "meshMapType")
MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard
}
set {
UserDefaults.standard.set(newValue, forKey: "meshMapType")
UserDefaults.standard.set(newValue.rawValue, forKey: "mapLayer")
}
}
@ -118,13 +106,31 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "enableOfflineMaps")
}
}
static var mapTileServer: String {
static var enableOfflineMapsMBTiles: Bool {
get {
UserDefaults.standard.string(forKey: "mapTileServer") ?? ""
UserDefaults.standard.bool(forKey: "enableOfflineMapsMBTiles")
}
set {
UserDefaults.standard.set(newValue, forKey: "mapTileServer")
UserDefaults.standard.set(newValue, forKey: "enableOfflineMapsMBTiles")
}
}
static var mapTileServer: MapTileServerLinks {
get {
MapTileServerLinks(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServerLinks.openStreetMap.rawValue) ?? MapTileServerLinks.openStreetMap
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer")
}
}
static var mapTilesAboveLabels: Bool {
get {
UserDefaults.standard.bool(forKey: "mapTilesAboveLabels")
}
set {
UserDefaults.standard.set(newValue, forKey: "mapTilesAboveLabels")
}
}
}

View file

@ -0,0 +1,161 @@
//
// OfflineTileManager.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 4/23/23.
//
import Foundation
import MapKit
class OfflineTileManager: ObservableObject {
enum DownloadStatus {
case download, downloading, downloaded
}
static let shared = OfflineTileManager()
init() {
print("Documents Directory = \(documentsDirectory)")
createDirectoriesIfNecessary()
}
// MARK: - Private properties
private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServerLinks.openStreetMap.tileUrl) }
private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! }
private let fileManager = FileManager.default
// MARK: - Public property
var progress: Float = 0
var status: DownloadStatus = .download
// MARK: - Public methods
func getAllDownloadedSize() -> String {
fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"))
}
func hasBeenDownloaded(for boundingBox: MKMapRect) -> Bool {
getEstimatedDownloadSize(for: boundingBox) == 0
}
func getEstimatedDownloadSize(for boundingBox: MKMapRect) -> Double {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
let count = self.filterTilesAlreadyExisting(paths: paths).count
let size: Double = 30000 // Bytes (average size)
return Double(count) * size
}
func getDownloadedSize(for boundingBox: MKMapRect) -> Double {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
var accumulatedSize: UInt64 = 0
for path in paths {
let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
let url = documentsDirectory.appendingPathComponent(file)
accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0
}
return Double(accumulatedSize)
}
func removeAll() {
try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles"))
createDirectoriesIfNecessary()
}
func remove(for boundingBox: MKMapRect) {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
for path in paths {
let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
let url = documentsDirectory.appendingPathComponent(file)
try? fileManager.removeItem(at: url)
}
self.status = .download
}
/// Download and persist all tiles within the boundingBox
func download(boundingBox: MKMapRect, name: String) {
NetworkManager.shared.runIfNetwork {
self.status = .downloading
self.progress = 0.01
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
let filteredPaths = self.filterTilesAlreadyExisting(paths: paths)
for i in 0..<filteredPaths.count {
self.persistLocally(path: filteredPaths[i])
self.progress = Float(i) / Float(filteredPaths.count)
}
DispatchQueue.main.async {
//NotificationManager.shared.sendNotification(title: "\("DownloadedTitle".localized) (\((self.getDownloadedSize(for: boundingBox)).toBytes))", message: "\("Downloaded".localized) (\(name))")
self.progress = 0
self.status = .downloaded
}
}
}
func getTileOverlay(for path: MKTileOverlayPath) -> URL {
let file = "\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
// Check is tile is already available
let tilesUrl = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file)
if fileManager.fileExists(atPath: tilesUrl.path){
return tilesUrl
} else {
if UserDefaults.enableOfflineMaps { // Get and persist newTile
return persistLocally(path: path)
} else { // Else display empty tile (transparent over Maps tiles)
return Bundle.main.url(forResource: "alpha", withExtension: "png")!
}
}
}
// MARK: - Private methods
private func computeTileOverlayPaths(boundingBox box: MKMapRect, maxZ: Int = 17) -> [MKTileOverlayPath] {
var paths = [MKTileOverlayPath]()
for z in 1...maxZ {
let topLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.minY).coordinate, zoom: z)
let topRight = tranformCoordinate(coordinates: MKMapPoint(x: box.maxX, y: box.minY).coordinate, zoom: z)
let bottomLeft = tranformCoordinate(coordinates: MKMapPoint(x: box.minX, y: box.maxY).coordinate, zoom: z)
for x in topLeft.x...topRight.x {
for y in topLeft.y...bottomLeft.y {
paths.append(MKTileOverlayPath(x: x, y: y, z: z, contentScaleFactor: 2))
}
}
}
return paths
}
private func tranformCoordinate(coordinates: CLLocationCoordinate2D , zoom: Int) -> TileCoordinates {
let lng = coordinates.longitude
let lat = coordinates.latitude
let tileX = Int(floor((lng + 180) / 360.0 * pow(2.0, Double(zoom))))
let tileY = Int(floor((1 - log( tan( lat * Double.pi / 180.0 ) + 1 / cos( lat * Double.pi / 180.0 )) / Double.pi ) / 2 * pow(2.0, Double(zoom))))
return (tileX, tileY, zoom)
}
@discardableResult private func persistLocally(path: MKTileOverlayPath) -> URL {
let url = overlay.url(forTilePath: path)
let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
let filename = documentsDirectory.appendingPathComponent(file)
do {
let data = try Data(contentsOf: url)
try data.write(to: filename)
} catch {
print("💀 Save Tile Error = \(error)")
}
return url
}
private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] {
paths.filter {
let file = "\(UserDefaults.mapTileServer.id)-z\($0.z)x\($0.x)y\($0.y).png"
let tilesPath = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file).path
return !fileManager.fileExists(atPath: tilesPath)
}
}
private func createDirectoriesIfNecessary() {
let tiles = documentsDirectory.appendingPathComponent("tiles")
try? fileManager.createDirectory(at: tiles, withIntermediateDirectories: true, attributes: [:])
}
}

View file

@ -0,0 +1,15 @@
//
// TileOverlay.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 5/5/23.
//
import Foundation
import MapKit
typealias TileCoordinates = (x: Int, y: Int, z: Int)
class TileOverlay: MKTileOverlay {
override func url(forTilePath path: MKTileOverlayPath) -> URL { OfflineTileManager.shared.getTileOverlay(for: path) }
}

View file

@ -24,13 +24,13 @@ struct Peripheral: Identifiable {
self.peripheral = peripheral
}
func getSignalStrength() -> SignalStrength {
func getSignalStrength() -> BLESignalStrength {
if NSNumber(value: rssi).compare(NSNumber(-65)) == ComparisonResult.orderedDescending {
return SignalStrength.strong
return BLESignalStrength.strong
} else if NSNumber(value: rssi).compare(NSNumber(-85)) == ComparisonResult.orderedDescending {
return SignalStrength.normal
return BLESignalStrength.normal
} else {
return SignalStrength.weak
return BLESignalStrength.weak
}
}
}

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

@ -32,7 +32,7 @@ import Foundation
import SwiftUI
struct SignalStrengthIndicator: View {
let signalStrength: SignalStrength
let signalStrength: BLESignalStrength
var body: some View {
HStack {
@ -40,7 +40,7 @@ struct SignalStrengthIndicator: View {
RoundedRectangle(cornerRadius: 3)
.divided(amount: (CGFloat(bar) + 1) / CGFloat(3))
.fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3))
.frame(width: 8, height: 30)
.frame(width: 8, height: 40)
}
}
}
@ -71,7 +71,7 @@ extension Shape {
}
}
enum SignalStrength: Int {
enum BLESignalStrength: Int {
case weak = 0
case normal = 1
case strong = 2

View file

@ -0,0 +1,71 @@
//
// LoRaSignalStrengthIndicator.swift
// Meshtastic
//
// Copyright Garth Vander Houwen 5/9/23.
//
import Foundation
import Foundation
import SwiftUI
struct LoRaSignalStrengthIndicator: View {
let signalStrength: LoRaSignalStrength
var body: some View {
HStack {
ForEach(0..<3) { bar in
RoundedRectangle(cornerRadius: 3)
.divided(amount: (CGFloat(bar) + 1) / CGFloat(3))
.fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3))
.frame(width: 8, height: 40)
}
}
}
private func getColor() -> Color {
switch signalStrength {
case .none:
return Color.red
case .bad:
return Color.orange
case .fair:
return Color.yellow
case .good:
return Color.green
}
}
}
enum LoRaSignalStrength: Int {
case none = 0
case bad = 1
case fair = 2
case good = 3
var description: String {
switch self {
case .none:
return "None"
case .bad:
return "Bad"
case .fair:
return "Fair"
case .good:
return "Good"
}
}
}
func getLoRaSignalStrength(snr: Float, rssi: Int32) -> LoRaSignalStrength {
if rssi > -115 && snr > -7 {
return .good
} else if rssi < -126 && snr < -15 {
return .none
} else if rssi <= -120 || snr <= -13 {
return .bad
} else {
return .fair
}
}

View file

@ -28,7 +28,7 @@ struct NodeInfoView: View {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
HStack {
VStack(alignment: .center) {
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 75, fontSize: 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white )
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 150, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 105 : 55, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white )
}
Divider()
VStack {
@ -41,31 +41,28 @@ struct NodeInfoView: View {
Text(String(hwModelString))
.foregroundColor(.gray)
.font(.largeTitle).fixedSize()
.font(.title).fixedSize()
}
}
if node.snr > 0 {
Divider()
Divider()
if node.snr != 0 {
VStack(alignment: .center) {
Image(systemName: "waveform.path")
.font(.title)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
.padding(.bottom, 10)
Text("SNR").font(.largeTitle).fixedSize()
Text("\(String(format: "%.2f", node.snr)) dB")
.font(.largeTitle)
let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi)
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
Text("Signal \(signalStrength.description)").font(.title)
Text("SNR \(String(format: "%.2f", node.snr))dB")
.foregroundColor(.gray)
.fixedSize()
.font(.title3)
Text("RSSI \(node.rssi)dB")
.foregroundColor(.gray)
.font(.title3)
}
Divider()
}
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
Divider()
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
if mostRecent?.voltage ?? 0 > 0.0 {
@ -142,7 +139,7 @@ struct NodeInfoView: View {
HStack {
VStack(alignment: .center) {
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white )
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 42 : 20, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white )
}
Divider()
VStack {
@ -152,31 +149,28 @@ struct NodeInfoView: View {
.frame(width: 75, height: 75)
.cornerRadius(5)
Text(String(node.user!.hwModel ?? "unset".localized))
.font(.callout).fixedSize()
.font(.caption).fixedSize()
}
}
if node.snr > 0 {
Divider()
Divider()
if node.snr != 0 {
VStack(alignment: .center) {
Image(systemName: "waveform.path")
.font(.title)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("SNR").font(.title2).fixedSize()
Text("\(String(format: "%.2f", node.snr)) dB")
.font(.title2)
let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi)
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
Text("Signal \(signalStrength.description)").font(.footnote)
Text("SNR \(String(format: "%.2f", node.snr))dB")
.foregroundColor(.gray)
.fixedSize()
.font(.caption2)
Text("RSSI \(node.rssi)dB")
.foregroundColor(.gray)
.font(.caption2)
}
Divider()
}
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
Divider()
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
if mostRecent?.voltage ?? 0 > 0 {

View file

@ -10,27 +10,30 @@ import MapKit
func degreesToRadians(_ number: Double) -> Double {
return number * .pi / 180
}
var currentMapLayer: MapLayer?
struct MapViewSwiftUI: UIViewRepresentable {
var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
var onWaypointEdit: (_ waypointId: Int ) -> Void
let mapView = MKMapView()
// Parameters
var selectedMapLayer: MapLayer
let positions: [PositionEntity]
let waypoints: [WaypointEntity]
let mapViewType: MKMapType
let userTrackingMode: MKUserTrackingMode
let showNodeHistory: Bool
let showRouteLines: Bool
let mapViewType: MKMapType = MKMapType.standard
// Offline Map Tiles
@AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
@State private var loadedLastUpdatedLocalMapFile = 0
var customMapOverlay: CustomMapOverlay?
@State private var presentCustomMapOverlayHash: CustomMapOverlay?
// Custom Tile Server
var tileRenderer: MKTileOverlayRenderer?
let tileServer: MapTileServerLinks = .openStreetMaps
// MARK: Private methods
@ -87,7 +90,36 @@ struct MapViewSwiftUI: UIViewRepresentable {
#endif
}
private func setMapLayer(mapView: MKMapView) {
// Avoid refreshing UI if selectedLayer has not changed
guard currentMapLayer != selectedMapLayer else { return }
currentMapLayer = selectedMapLayer
for overlay in mapView.overlays {
if overlay is MKTileOverlay {
mapView.removeOverlay(overlay)
}
}
switch selectedMapLayer {
case .offline:
mapView.mapType = .standard
if !UserDefaults.enableOfflineMapsMBTiles {
let overlay = TileOverlay()
overlay.canReplaceMapContent = false
//overlay.minimumZ = 0
//overlay.maximumZ = 17
mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads)
}
case .satellite:
mapView.mapType = .satellite
case .hybrid:
mapView.mapType = .hybrid
default:
mapView.mapType = .standard
}
}
func makeUIView(context: Context) -> MKMapView {
currentMapLayer = nil
mapView.delegate = context.coordinator
self.configureMap(mapView: mapView)
return mapView
@ -95,36 +127,13 @@ struct MapViewSwiftUI: UIViewRepresentable {
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapViewType
// Offline maps and tile server settings
if UserDefaults.enableOfflineMaps {
// MBTiles Offline
if UserDefaults.enableOfflineMaps && UserDefaults.enableOfflineMapsMBTiles {
if UserDefaults.mapTileServer.count > 0 {
tileRenderer?.alpha = 0.0
let overlays = mapView.overlays
if mapView.mapType == .standard {
let overlay = MKTileOverlay(urlTemplate: UserDefaults.mapTileServer)
if overlays.contains(where: {$0 is MKPolyline}) {
mapView.addOverlay(overlay, level: .aboveLabels)
if let poly_overlay = overlays.filter({$0 is MKPolyline}).first {
mapView.addOverlay(poly_overlay, level: .aboveLabels)
}
} else {
mapView.addOverlay(overlay, level: .aboveLabels)
}
} else {
for overlay in overlays {
if let ove = overlay as? MKTileOverlay {
mapView.removeOverlay(ove)
}
}
}
} else if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
mapView.removeOverlays(mapView.overlays)
if self.customMapOverlay != nil {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
@ -144,11 +153,13 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
}
}
// Set selected map layer
setMapLayer(mapView: mapView)
let latest = positions
.filter { $0.latest == true }
.sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
// Node Route Lines
if showRouteLines {
// Remove all existing PolyLine Overlays
@ -188,28 +199,24 @@ struct MapViewSwiftUI: UIViewRepresentable {
print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)")
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(waypoints)
mapView.addAnnotations(showNodeHistory ? positions : latest)
}
if userTrackingMode == MKUserTrackingMode.none {
mapView.showsUserLocation = false
if UserDefaults.enableMapRecentering {
if annotationCount != mapView.annotations.count {
mapView.addAnnotations(showNodeHistory ? positions : latest)
}
if latest.count > 1 {
mapView.fitAllAnnotations()
if latest.count == 1 {
mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true)
} else {
mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false)
mapView.fitAllAnnotations()
}
}
} else {
// Centering Done by tracking mode
if annotationCount != mapView.annotations.count {
mapView.addAnnotations(showNodeHistory ? positions : latest)
}
mapView.showsUserLocation = true
}
mapView.setUserTrackingMode(userTrackingMode, animated: true)
}

View file

@ -0,0 +1,49 @@
//
// TilesView.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen on 5/6/23.
//
import SwiftUI
import MapKit
struct TilesView: View {
@ObservedObject var tileManager = OfflineTileManager.shared
@State var totalDownloadedTileSize = ""
var body: some View {
Button(action: {
tileManager.removeAll()
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
print("delete all tiles")
}) {
HStack {
Image(systemName: "trash")
.font(.callout)
.foregroundColor(.red)
Text("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))")
.font(.callout)
.foregroundColor(.red)
Spacer()
}
}
.onAppear(perform: {
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
})
}
}
// MARK: Previews
struct TilesView_Previews: PreviewProvider {
static var previews: some View {
TilesView()
.previewLayout(.fixed(width: 300, height: 80))
.environment(\.colorScheme, .light)
}
}

View file

@ -148,7 +148,7 @@ struct Contacts: View {
HStack {
VStack {
HStack {
CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: 18, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white)
CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: (user.shortName ?? "???").isEmoji() ? 42 : 20, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white)
.padding(.trailing, 5)
VStack {
HStack {

View file

@ -16,7 +16,9 @@ struct NodeDetail: View {
@AppStorage("meshMapType") private var meshMapType = 0
@AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
@AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
@State private var mapType: MKMapType = .standard
//@State private var mapType: MKMapType = .standard
@State private var selectedMapLayer: MapLayer = .standard
@State var mapRect: MKMapRect = MKMapRect()
@State var waypointCoordinate: WaypointCoordinate?
@State var editingWaypoint: Int = 0
@State private var loadedWeather: Bool = false
@ -68,8 +70,12 @@ struct NodeDetail: View {
if wpId > 0 {
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
}
}, positions: lastTenThousand, waypoints: Array(waypoints),
mapViewType: mapType,
},
//visibleMapRect: $mapRect,
selectedMapLayer: selectedMapLayer,
positions: lastTenThousand,
waypoints: Array(waypoints),
//mapViewType: mapType,
userTrackingMode: MKUserTrackingMode.none,
showNodeHistory: meshMapShowNodeHistory,
showRouteLines: meshMapShowRouteLines,
@ -79,18 +85,18 @@ struct NodeDetail: View {
Spacer()
HStack(alignment: .bottom, spacing: 1) {
Picker("Map Type", selection: $mapType) {
ForEach(MeshMapTypes.allCases) { map in
Text(map.description)
.tag(map.MKMapTypeValue())
}
}
.onChange(of: (mapType)) { newMapType in
UserDefaults.mapType = Int(newMapType.rawValue)
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.pickerStyle(.menu)
.padding(5)
// Picker("Map Type", selection: $mapType) {
// ForEach(MeshMapTypes.allCases) { map in
// Text(map.description)
// .tag(map.MKMapTypeValue())
// }
// }
// .onChange(of: (mapType)) { newMapType in
// UserDefaults.mapType = Int(newMapType.rawValue)
// }
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
// .pickerStyle(.menu)
// .padding(5)
VStack {
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
.font(.caption)
@ -225,7 +231,7 @@ struct NodeDetail: View {
})
.onAppear {
self.bleManager.context = context
mapType = MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard
//mapType = .standard// MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard
}
.task(id: node.num) {
if !loadedWeather {

View file

@ -35,7 +35,7 @@ struct NodeList: View {
let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num)
VStack(alignment: .leading) {
HStack {
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white)
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : 22, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white)
.padding(.trailing, 5)
VStack(alignment: .leading) {
Text(node.user?.longName ?? "unknown".localized).font(.headline)

View file

@ -14,13 +14,17 @@ struct NodeMap: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@State var meshMapType: Int = UserDefaults.mapType
@State var selectedMapLayer: MapLayer = UserDefaults.mapLayer
@State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering
@State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines
@State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins
@State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps
@State var mapTileServer: String = UserDefaults.mapTileServer
@State var selectedTileServer: MapTileServerLinks = UserDefaults.mapTileServer
@State var enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles
@State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none)
@ -31,13 +35,12 @@ struct NodeMap: View {
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@State var waypointCoordinate: WaypointCoordinate?
@State var mapType: MKMapType = .standard
@State var selectedTracking: UserTrackingModes = .none
@State var selectedTileServer: MapTileServerLinks = .wikimedia
@State var isPresentingInfoSheet: Bool = false
@State var waypointCoordinate: WaypointCoordinate?
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
mapName: "offlinemap",
tileType: "png",
@ -57,9 +60,9 @@ struct NodeMap: View {
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
}
},
selectedMapLayer: selectedMapLayer,
positions: Array(positions),
waypoints: Array(waypoints),
mapViewType: mapType,
userTrackingMode: selectedTracking.MKUserTrackingModeValue(),
showNodeHistory: enableMapNodeHistoryPins,
showRouteLines: enableMapRouteLines,
@ -89,15 +92,21 @@ struct NodeMap: View {
VStack {
Form {
Section(header: Text("Map Options")) {
Picker("Map Type", selection: $mapType) {
ForEach(MeshMapTypes.allCases) { map in
Text(map.description).tag(map.MKMapTypeValue())
Picker(selection: $selectedMapLayer, label: Text("")) {
ForEach(MapLayer.allCases, id: \.self) { layer in
if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
Text(layer.localized)
} else if layer != MapLayer.offline {
Text(layer.localized)
}
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (mapType)) { newMapType in
UserDefaults.mapType = Int(newMapType.rawValue)
.pickerStyle(SegmentedPickerStyle())
.onChange(of: (selectedMapLayer)) { newMapLayer in
UserDefaults.mapLayer = newMapLayer
}
.padding(.top, 5)
.padding(.bottom, 5)
Toggle(isOn: $enableMapRecentering) {
@ -137,39 +146,60 @@ struct NodeMap: View {
.onTapGesture {
self.enableOfflineMaps.toggle()
UserDefaults.enableOfflineMaps = self.enableOfflineMaps
}
Text("If you have shared a MBTiles file with meshtastic it will be loaded.")
.font(.caption)
.foregroundColor(.gray)
if UserDefaults.enableOfflineMaps {
HStack {
// Picker("Tile Servers", selection: $selectedTileServer) {
// ForEach(MapTileServerLinks.allCases) { ts in
// Text(ts.description)
// .tag(ts.id)
// }
// }
// .pickerStyle(.menu)
// .onChange(of: (selectedTileServer)) { newTileServer in
//
// mapTileServer = selectedTileServer.tileUrl
// }
Label("Tile Server", systemImage: "square.grid.3x2")
TextField(
"Tile Server",
text: $mapTileServer,
axis: .vertical
)
.foregroundColor(.gray)
.font(.caption)
.onChange(of: (mapTileServer)) { newMapTileServer in
UserDefaults.mapTileServer = newMapTileServer
if !self.enableOfflineMaps {
if self.selectedMapLayer == .offline {
self.selectedMapLayer = .standard
}
}
.keyboardType(.asciiCapable)
.disableAutocorrection(true)
}
if UserDefaults.enableOfflineMaps {
VStack (alignment: .leading) {
if !enableOfflineMapsMBTiles {
Picker(selection: $selectedTileServer,
label: Text("Tile Server")) {
ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in
Text(tsl.description)
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedTileServer)) { newSelectedTileServer in
UserDefaults.mapTileServer = newSelectedTileServer
//tileManager.removeAll()
selectedMapLayer = .standard
}
Text("Attribution:")
.fontWeight(.semibold)
.font(.footnote)
Text(LocalizedStringKey(selectedTileServer.attribution))
.font(.footnote)
.foregroundColor(.gray)
.padding(0)
Divider()
Toggle(isOn: $mapTilesAboveLabels) {
Text("Tiles above Labels")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onTapGesture {
self.mapTilesAboveLabels.toggle()
UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels
}
}
Divider()
Toggle(isOn: $enableOfflineMapsMBTiles) {
Text("Enable MB Tiles")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onTapGesture {
self.enableOfflineMapsMBTiles.toggle()
UserDefaults.enableOfflineMapsMBTiles = self.enableOfflineMapsMBTiles
}
Text("The latest MBTiles file shared with meshtastic will be loaded into the map.")
.font(.footnote)
.foregroundColor(.gray)
}
}
}
}
@ -185,7 +215,7 @@ struct NodeMap: View {
.padding(.bottom)
#endif
}
.presentationDetents([.medium, .large])
.presentationDetents([UserDefaults.enableOfflineMaps ? .large : .medium])
.presentationDragIndicator(.visible)
}
}
@ -201,8 +231,6 @@ struct NodeMap: View {
.onAppear(perform: {
UIApplication.shared.isIdleTimerDisabled = true
self.bleManager.context = context
mapType = MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard
})
.onDisappear(perform: {
UIApplication.shared.isIdleTimerDisabled = false

View file

@ -60,10 +60,6 @@ struct AppSettings: View {
Label("provide.location", systemImage: "location.circle.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onTapGesture {
self.provideLocation.toggle()
UserDefaults.provideLocation = self.provideLocation
}
if UserDefaults.provideLocation {
@ -81,7 +77,9 @@ struct AppSettings: View {
.font(.caption)
.foregroundColor(.gray)
}
}
TilesView()
}
HStack {
Button {
@ -119,8 +117,8 @@ struct AppSettings: View {
.onChange(of: (meshtasticUsername)) { newMeshtasticUsername in
UserDefaults.meshtasticUsername = newMeshtasticUsername
}
.onChange(of: provideLocation) { _ in
.onChange(of: provideLocation) { newProvideLocation in
UserDefaults.provideLocation = newProvideLocation
if bleManager.connectedPeripheral != nil {
self.bleManager.sendWantConfig()
}

View file

@ -79,7 +79,7 @@ struct UserConfig: View {
})
.foregroundColor(.gray)
}
.keyboardType(.asciiCapable)
.keyboardType(.default)
.disableAutocorrection(true)
Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.")
.font(.caption2)

View file

@ -138,6 +138,7 @@
"lora.config"="LoRa Einstellungen";
"map"="Mesh Karte";
"map.centering"="Centering";
"map.tiles.delete"="Delete Cached Map Tiles";
"map.recentering"="Automatic Re-centering";
"map.type"="kartentyp";
"map.usertrackingmode"="User tracking mode";
@ -197,6 +198,7 @@
"not.connected"="Kein Gerät verbunden";
"numbers.punctuation"="Ziffern und Interpunktion";
"off"="Aus";
"offline"="Offline";
"on.boot"="Nur beim Starten";
"options"="Optionen";
"password"="Passwort";

View file

@ -139,6 +139,7 @@
"map"="Mesh Map";
"map.type"="Default Type";
"map.centering"="Centering Mode";
"map.tiles.delete"="Delete Cached Map Tiles";
"map.recentering"="Automatic Re-centering";
"map.usertrackingmode"="User tracking mode";
"map.usertrackingmode.follow"="Follow";
@ -197,6 +198,7 @@
"not.connected"="No device connected";
"numbers.punctuation"="Numbers and Punctuation";
"off"="Off";
"offline"="Offline";
"on.boot"="On Boot Only";
"options"="Options";
"password"="Password";

View file

@ -138,6 +138,7 @@
"lora.config"="LoRa 配置";
"map"="Mesh 地图";
"map.centering"="Centering";
"map.tiles.delete"="Delete Cached Map Tiles";
"map.recentering"="Automatic Re-centering";
"map.type"="地图类型";
"map.usertrackingmode"="User tracking mode";
@ -197,6 +198,7 @@
"not.connected"="未连接到电台";
"numbers.punctuation"="数字和标点符号";
"off"="关闭";
"offline"="Offline";
"on.boot"="仅在启动时";
"options"="选项";
"password"="密码";