mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #357 from meshtastic/offline_maps_updates
Offline maps updates
This commit is contained in:
commit
791c3d216b
26 changed files with 781 additions and 234 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
65
Meshtastic/Extensions/FileManager.swift
Normal file
65
Meshtastic/Extensions/FileManager.swift
Normal 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
21
Meshtastic/Extensions/Url.swift
Normal file
21
Meshtastic/Extensions/Url.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
161
Meshtastic/Helpers/Map/OfflineTileManager.swift
Normal file
161
Meshtastic/Helpers/Map/OfflineTileManager.swift
Normal 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: [:])
|
||||
}
|
||||
|
||||
}
|
||||
15
Meshtastic/Helpers/Map/TileOverlay.swift
Normal file
15
Meshtastic/Helpers/Map/TileOverlay.swift
Normal 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) }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
|
@ -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
|
||||
71
Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift
Normal file
71
Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
49
Meshtastic/Views/Map/Custom/TilesView.swift
Normal file
49
Meshtastic/Views/Map/Custom/TilesView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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"="密码";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue