diff options
author | Iván Ávalos <avalos@disroot.org> | 2022-02-03 02:33:50 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2022-02-03 02:33:50 -0600 |
commit | 48e9f6e38213c533286d83e426346471d507467d (patch) | |
tree | fa19d37b1ccdcd17ea40181ae3bef6abedb2450e | |
parent | 416be1cc70494737efc2fc2409c889db314d7fdd (diff) | |
download | etbsa-trackermap-mobile-48e9f6e38213c533286d83e426346471d507467d.tar.gz etbsa-trackermap-mobile-48e9f6e38213c533286d83e426346471d507467d.tar.bz2 etbsa-trackermap-mobile-48e9f6e38213c533286d83e426346471d507467d.zip |
WIP: rewrote MapWrapperView + MapView as unified UIKit view controller
-rw-r--r-- | iosApp/iosApp.xcodeproj/project.pbxproj | 50 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapView.swift | 307 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapViewController.xib | 104 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapWrapperView.swift | 87 | ||||
-rw-r--r-- | iosApp/iosApp/Map/UnitMapView.swift | 6 | ||||
-rw-r--r-- | iosApp/iosApp/Shared/HyperlinkText.swift | 186 |
6 files changed, 242 insertions, 498 deletions
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index f155c39..9f2f41f 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -19,10 +19,11 @@ E33A236E27A7545500DD647F /* WhirlyGlobeMaplyComponent.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E33A236B27A7543700DD647F /* WhirlyGlobeMaplyComponent.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E33A237127A7553500DD647F /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A237027A7553500DD647F /* MapView.swift */; }; E33A237327A7581A00DD647F /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A237227A7581A00DD647F /* Utils.swift */; }; - E34A2F4427A77D9500AD8AEB /* MapWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */; }; E34A2F4827A7878200AD8AEB /* HyperlinkText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34A2F4727A7878200AD8AEB /* HyperlinkText.swift */; }; E34A2F4B27A7881200AD8AEB /* SwiftUIFlowLayout in Frameworks */ = {isa = PBXBuildFile; productRef = E34A2F4A27A7881200AD8AEB /* SwiftUIFlowLayout */; }; E34A2F4D27A7DB2200AD8AEB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E34A2F4C27A7DB2200AD8AEB /* Localizable.strings */; }; + E36DF77B27AB740C003C561C /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36DF77927AB740C003C561C /* MapViewController.swift */; }; + E36DF77C27AB740C003C561C /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E36DF77A27AB740C003C561C /* MapViewController.xib */; }; E38F241527A242870069FC45 /* Inject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241427A242870069FC45 /* Inject.swift */; }; E38F241727A242C70069FC45 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241627A242C70069FC45 /* Resolver.swift */; }; E38F241C27A26DD70069FC45 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241B27A26DD70069FC45 /* RootViewModel.swift */; }; @@ -61,9 +62,10 @@ E33A236B27A7543700DD647F /* WhirlyGlobeMaplyComponent.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WhirlyGlobeMaplyComponent.xcframework; sourceTree = "<group>"; }; E33A237027A7553500DD647F /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; }; E33A237227A7581A00DD647F /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; }; - E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapWrapperView.swift; sourceTree = "<group>"; }; E34A2F4727A7878200AD8AEB /* HyperlinkText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HyperlinkText.swift; sourceTree = "<group>"; }; E34A2F4C27A7DB2200AD8AEB /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; }; + E36DF77927AB740C003C561C /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; }; + E36DF77A27AB740C003C561C /* MapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MapViewController.xib; sourceTree = "<group>"; }; E38F241427A242870069FC45 /* Inject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inject.swift; sourceTree = "<group>"; }; E38F241627A242C70069FC45 /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; }; E38F241B27A26DD70069FC45 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; }; @@ -119,6 +121,7 @@ children = ( E34A2F4C27A7DB2200AD8AEB /* Localizable.strings */, E33A235E27A4FD1C00DD647F /* Map */, + E35A078427AB615F00F24D71 /* Details */, E39ABC4427A4EBB000965D05 /* Devices */, E38F241A27A2659C0069FC45 /* Units */, E3E77EE1279D43C000150070 /* Shared */, @@ -144,11 +147,43 @@ children = ( E33A235F27A4FD2C00DD647F /* UnitMapView.swift */, E33A237027A7553500DD647F /* MapView.swift */, - E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */, + E36DF77927AB740C003C561C /* MapViewController.swift */, + E36DF77A27AB740C003C561C /* MapViewController.xib */, ); path = Map; sourceTree = "<group>"; }; + E35A078427AB615F00F24D71 /* Details */ = { + isa = PBXGroup; + children = ( + E35A078727AB619700F24D71 /* Reports */, + E35A078627AB619000F24D71 /* Information */, + E35A078527AB618700F24D71 /* Commands */, + ); + path = Details; + sourceTree = "<group>"; + }; + E35A078527AB618700F24D71 /* Commands */ = { + isa = PBXGroup; + children = ( + ); + path = Commands; + sourceTree = "<group>"; + }; + E35A078627AB619000F24D71 /* Information */ = { + isa = PBXGroup; + children = ( + ); + path = Information; + sourceTree = "<group>"; + }; + E35A078727AB619700F24D71 /* Reports */ = { + isa = PBXGroup; + children = ( + ); + path = Reports; + sourceTree = "<group>"; + }; E38F241A27A2659C0069FC45 /* Units */ = { isa = PBXGroup; children = ( @@ -229,6 +264,7 @@ TargetAttributes = { 7555FF7A242A565900829871 = { CreatedOnToolsVersion = 11.3.1; + LastSwiftMigration = 1320; }; }; }; @@ -263,6 +299,7 @@ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, E34A2F4D27A7DB2200AD8AEB /* Localizable.strings in Resources */, 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + E36DF77C27AB740C003C561C /* MapViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -300,12 +337,12 @@ E38F241527A242870069FC45 /* Inject.swift in Sources */, E39ABC4827A4EDEC00965D05 /* DeviceRow.swift in Sources */, E38F241C27A26DD70069FC45 /* RootViewModel.swift in Sources */, + E36DF77B27AB740C003C561C /* MapViewController.swift in Sources */, E33A237327A7581A00DD647F /* Utils.swift in Sources */, E3E77EE6279E6CE400150070 /* FlowCollector.swift in Sources */, E33A236527A530F300DD647F /* SmallLabelStyle.swift in Sources */, E39ABC4327A4E88C00965D05 /* UnitsViewModel.swift in Sources */, E34A2F4827A7878200AD8AEB /* HyperlinkText.swift in Sources */, - E34A2F4427A77D9500AD8AEB /* MapWrapperView.swift in Sources */, 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, E39ABC4627A4EBD500965D05 /* DevicesView.swift in Sources */, E38F242027A27B550069FC45 /* LoadingView.swift in Sources */, @@ -437,6 +474,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = 358YRZ9P3L; @@ -458,6 +496,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = mx.trackermap.TrackerMap; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -467,6 +507,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = 358YRZ9P3L; @@ -488,6 +529,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = mx.trackermap.TrackerMap; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/iosApp/iosApp/Map/MapView.swift b/iosApp/iosApp/Map/MapView.swift index 572db64..6b0fa20 100644 --- a/iosApp/iosApp/Map/MapView.swift +++ b/iosApp/iosApp/Map/MapView.swift @@ -21,310 +21,35 @@ import WhirlyGlobeMaplyComponent import shared struct MapView: UIViewControllerRepresentable { - typealias UIViewControllerType = OurMaplyViewController + typealias UIViewControllerType = MapViewController - @Binding var mapLayer: MapLayer + @Binding var layer: MapLayer @Binding var markers: [Marker] - var markerCallback: ((Int32?) -> Void)? - var link: BaseMapLink + var markerCallback: MarkerCallback? - class Coordinator: NSObject, MaplyViewControllerDelegate { - var parent: MapView - var uiViewController: OurMaplyViewController? - var loader: MaplyQuadImageLoader? = nil - - // Source: https://stackoverflow.com/questions/65923718 - var cancellable: AnyCancellable? - var link: BaseMapLink? { - didSet { - cancellable = link?.$action.sink(receiveValue: { action in - guard let action = action else { - return - } - self.uiViewController?.action(action) - }) - } - } - - init(_ uiViewController: MapView) { - self.parent = uiViewController - } - - func maplyViewController(_ viewC: MaplyViewController, - didTapAt coord: MaplyCoordinate) { - if let callback = parent.markerCallback { - callback(nil) - } - } - - func maplyViewController(_ viewC: MaplyViewController, - didSelect selectedObj: NSObject, - atLoc coord: MaplyCoordinate, - onScreen screenPt: CGPoint) { - if let marker = selectedObj as? MaplyScreenMarker { - if let id = marker.userObject as? Int32 { - if let callback = parent.markerCallback { - callback(id) - } - } - } - } + class Coordinator { + var shouldCenter: Bool = true } func makeCoordinator() -> Coordinator { - Coordinator(self) + return Coordinator() } - func makeUIViewController(context: Context) -> OurMaplyViewController { - let mapViewController = OurMaplyViewController(mapType: .typeFlat) - mapViewController.delegate = context.coordinator - - let tileInfo = Utils.tileInfoFrom(layer: mapLayer) - mapViewController.setZoomLimits(minZoom: mapLayer.minZoom, - maxZoom: mapLayer.maxZoom) - - let sampleParams = MaplySamplingParams() - sampleParams.coordSys = MaplySphericalMercator(webStandard: ()) - sampleParams.coverPoles = true - sampleParams.edgeMatching = true - sampleParams.minZoom = tileInfo.minZoom - sampleParams.maxZoom = tileInfo.maxZoom - sampleParams.singleLevel = true - sampleParams.maxTiles = 25 - - let loader = MaplyQuadImageLoader(params: sampleParams, tileInfo: tileInfo, viewC: mapViewController) - loader?.baseDrawPriority = kMaplyImageLayerDrawPriorityDefault - loader?.imageFormat = .imageUShort565 - context.coordinator.loader = loader - - DispatchQueue.main.async { - let point = MaplyCoordinateMakeWithDegrees(-100.36, 23.191) - mapViewController.focusOn(point: point, height: 0.4, animated: false) - } - - return mapViewController + func makeUIViewController(context: Context) -> MapViewController { + let mapVC = MapViewController() + mapVC.markerCallback = markerCallback + mapVC.mapLayer = layer + return mapVC } - func updateUIViewController(_ uiViewController: OurMaplyViewController, context: Context) { - context.coordinator.uiViewController = uiViewController - context.coordinator.link = link - + func updateUIViewController(_ uiViewController: MapViewController, context: Context) { // MARK: - Set map layer - context.coordinator.loader?.changeTileInfo(Utils.tileInfoFrom(layer: mapLayer)) - uiViewController.setZoomLimits(minZoom: mapLayer.minZoom, - maxZoom: mapLayer.maxZoom) + uiViewController.setMapLayer(layer) // MARK: - Set markers uiViewController.display(markers: markers, - isReport: false) - } - - static func dismantleUIViewController(_ uiViewController: MaplyViewController, coordinator: Coordinator) { - coordinator.loader?.shutdown() - uiViewController.teardown() - } -} - -class OurMaplyViewController: MaplyViewController { - enum Action { - case zoomIn - case zoomOut - } - - private var objects = [MaplyComponentObject]() - private var geofenceObjects = [MaplyComponentObject]() - - func action(_ action: Action) { - DispatchQueue.main.async { - switch action { - case .zoomIn: - self.zoomIn() - case .zoomOut: - self.zoomOut() - } - } - } - - func focusOn(point: MaplyCoordinate, - height: Float = 0.0000264, - animated: Bool = true) { - let z = max(height, getMinZoom()) - if animated { - animate(toPosition: point, height: z, time: 0.2) - } else { - setPosition(point, height: z) - } - } - - func zoomIn() { - let pos = getPosition() - let zoom = currentMapScale() / 2 - focusOn(point: pos, height: height(forMapScale: zoom)) - } - - func zoomOut() { - let pos = getPosition() - let zoom = currentMapScale() * 2 - focusOn(point: pos, height: height(forMapScale: zoom)) - } - - func clear(geofences: Bool = false) { - if geofences { - remove(geofenceObjects) - geofenceObjects.removeAll() - } else { - remove(objects) - objects.removeAll() - } - } - - func display(markers: [Marker], - isReport: Bool, - center: Bool = false) { - clear() - - let points = markers.map { marker in - MaplyCoordinateMakeWithDegrees(Float(marker.longitude), - Float(marker.latitude)) - } - - let fontSize = 11.0 - let colorReport = Color.green - let colorLabel = Color.secondary - let colorLabelOutline = Color.systemBackground - - let vectorDesc: [AnyHashable : Any] = [ - kMaplyColor: colorReport, - kMaplyVecWidth: 20.0 - ] - - let labelDesc: [AnyHashable : Any] = [ - kMaplyFont: UIFont.boldSystemFont(ofSize: fontSize), - kMaplyTextColor: colorLabel, - kMaplyTextOutlineColor: colorLabelOutline, - kMaplyTextOutlineSize: 3.0 - ] - - /* MARK: - Draw markers for positions */ - let screenMarkers = markers.enumerated().map { (i, marker) -> MaplyScreenMarker in - let screenMarker = MaplyScreenMarker() - screenMarker.layoutImportance = .greatestFiniteMagnitude - screenMarker.loc = MaplyCoordinateMakeWithDegrees(Float(marker.longitude), - Float(marker.latitude)) - var type: Marker.Type_ = .default_ - if isReport { - // For reports, position, start and end icons must be different - switch i { - case markers.startIndex: type = .reportStart - case markers.endIndex: type = .reportEnd - default: type = .reportPosition - } - } else { - type = marker.type - } - screenMarker.image = getIcon(markerType: type) - - var size = 50.0 - if isReport { - // For reports, position, start and end sizes must be different - switch i { - case markers.startIndex: size = 50.0 - case markers.endIndex: size = 50.0 - default: size = 25.0 - } - } - screenMarker.size = CGSize(width: size, height: size) - screenMarker.userObject = marker.id - screenMarker.selectable = true - - return screenMarker - } - - if let objs = addScreenMarkers(screenMarkers, desc: nil, mode: .any) { - objects.append(objs) - } - - /* MARK: - Add labels for markers */ - if !isReport && !markers.isEmpty { - let screenLabels = markers.map { marker -> MaplyScreenLabel in - let label = MaplyScreenLabel() - label.layoutImportance = .greatestFiniteMagnitude - var text = marker.name - if marker.name.count >= 20 { - let end = marker.name.index(marker.name.startIndex, offsetBy: 20) - text = String(marker.name[..<end]) - } - label.text = text - label.loc = MaplyCoordinateMakeWithDegrees(Float(marker.longitude), - Float(marker.latitude)) - label.offset = CGPoint(x: 0.0, y: 25.0) - - return label - } - - if let objs = addScreenLabels(screenLabels, desc: labelDesc) { - objects.append(objs) - } - } - - /* MARK: - Draw polyline for report */ - if isReport && !markers.isEmpty { - let geoJSON: [AnyHashable : Any] = [ - "type": "FeatureCollection", - "features": [ - [ - "type": "LineString", - "coordinates": points.map({ point in - [point.x, point.y] - }) - ] - ] - ] - if let vector = MaplyVectorObject(fromGeoJSONDictionary: geoJSON) { - if let objs = addVectors([vector], desc: vectorDesc, mode: .any) { - objects.append(objs) - } - } - } - - /* MARK: - Center map to bounds */ - if center && !markers.isEmpty { - let box = MaplyBoundingBoxExpandByFraction( - MaplyBoundingBoxFromCoordinates(points, UInt32(points.count)), 0.1) - let center = MaplyCoordinate(x: (box.ur.x + box.ll.x) / 2, - y: (box.ur.y + box.ll.y) / 2) - let zoom = max(findHeight(toViewBounds: box, pos: center), getMinZoom()) - setPosition(center, height: zoom) - } - } - - func setZoomLimits(minZoom: Int32, maxZoom: Int32) { - setZoomLimitsMin( - height(forMapScale: Float(truncating: - MapCalculus.companion.zoomLevelToScale(zoom: maxZoom) - ?? MapCalculus.companion.zoomLevelToScale(zoom: 21)! - )), - max: height(forMapScale: Float(truncating: - MapCalculus.companion.zoomLevelToScale(zoom: minZoom) - ?? MapCalculus.companion.zoomLevelToScale(zoom: 1)! - ))) - } - - private func getIcon(markerType: Marker.Type_) -> UIImage { - return UIImage(named: MarkerTransformations - .markerTypeToImageName(markerType: markerType))! - } -} - -// Source: https://stackoverflow.com/questions/65923718 -class BaseMapLink: ObservableObject { - @Published var action: OurMaplyViewController.Action? - - func zoomIn() { - action = .zoomIn - } - - func zoomOut() { - action = .zoomOut + isReport: false, + center: context.coordinator.shouldCenter) + context.coordinator.shouldCenter = false } } diff --git a/iosApp/iosApp/Map/MapViewController.xib b/iosApp/iosApp/Map/MapViewController.xib new file mode 100644 index 0000000..9d1694f --- /dev/null +++ b/iosApp/iosApp/Map/MapViewController.xib @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> + <device id="retina5_9" orientation="portrait" appearance="light"/> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/> + <capability name="Image references" minToolsVersion="12.0"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="System colors in document resources" minToolsVersion="11.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <objects> + <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MapViewController" customModule="iosApp" customModuleProvider="target"> + <connections> + <outlet property="attributionText" destination="R7q-da-DO1" id="Noy-FR-ZRo"/> + <outlet property="mapContainer" destination="T6C-8E-ftt" id="esY-iz-qsl"/> + <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/> + </connections> + </placeholder> + <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> + <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT"> + <rect key="frame" x="0.0" y="0.0" width="375" height="812"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="XUh-lD-GKm"> + <rect key="frame" x="317" y="52" width="50" height="50"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <constraints> + <constraint firstAttribute="height" constant="50" id="SFA-XB-o3z"/> + <constraint firstAttribute="width" constant="50" id="ezA-Bu-6Mq"/> + </constraints> + <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> + <state key="normal"> + <color key="titleColor" systemColor="labelColor"/> + <imageReference key="image" image="plus" catalog="system" symbolScale="large"/> + <preferredSymbolConfiguration key="preferredSymbolConfiguration"/> + </state> + <connections> + <action selector="onZoomInPressed:" destination="-1" eventType="touchUpInside" id="x4P-sk-jdi"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kdu-l3-IK7"> + <rect key="frame" x="317" y="110" width="50" height="50"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <constraints> + <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="50" id="2yG-8e-OLn"/> + <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="50" id="ACq-Cq-PTq"/> + <constraint firstAttribute="width" constant="50" id="Ep3-3d-iue"/> + <constraint firstAttribute="height" constant="50" id="gMl-lS-Lvw"/> + </constraints> + <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> + <state key="normal"> + <color key="titleColor" systemColor="labelColor"/> + <imageReference key="image" image="minus" catalog="system" symbolScale="large"/> + <preferredSymbolConfiguration key="preferredSymbolConfiguration"/> + </state> + <connections> + <action selector="onZoomOutPressed:" destination="-1" eventType="touchUpInside" id="rt2-fy-VCy"/> + </connections> + </button> + <textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" alpha="0.40000000000000002" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Copyright (C) 2020 OpenStreetMap" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R7q-da-DO1"> + <rect key="frame" x="0.0" y="745" width="375" height="33"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <color key="textColor" systemColor="labelColor"/> + <fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="14"/> + <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> + </textView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="T6C-8E-ftt"> + <rect key="frame" x="0.0" y="44" width="375" height="734"/> + <color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/> + </view> + </subviews> + <viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <constraints> + <constraint firstItem="T6C-8E-ftt" firstAttribute="trailing" secondItem="fnl-2z-Ty3" secondAttribute="trailing" id="Adi-2F-rXH"/> + <constraint firstItem="T6C-8E-ftt" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="EGw-gK-1tb"/> + <constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="XUh-lD-GKm" secondAttribute="trailing" constant="8" id="END-FB-FNk"/> + <constraint firstItem="T6C-8E-ftt" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="HzR-cl-3zE"/> + <constraint firstItem="R7q-da-DO1" firstAttribute="bottom" secondItem="fnl-2z-Ty3" secondAttribute="bottom" id="NMu-yb-3sE"/> + <constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="kdu-l3-IK7" secondAttribute="trailing" constant="8" id="Um2-vS-uoQ"/> + <constraint firstItem="R7q-da-DO1" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="XM1-Gu-sfE"/> + <constraint firstItem="kdu-l3-IK7" firstAttribute="top" secondItem="XUh-lD-GKm" secondAttribute="bottom" constant="8" id="hTB-Wm-3eQ"/> + <constraint firstItem="R7q-da-DO1" firstAttribute="trailing" secondItem="fnl-2z-Ty3" secondAttribute="trailing" id="heK-zH-Ob4"/> + <constraint firstItem="T6C-8E-ftt" firstAttribute="bottom" secondItem="fnl-2z-Ty3" secondAttribute="bottom" id="oT6-iC-7PW"/> + <constraint firstItem="XUh-lD-GKm" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" constant="8" id="oXT-Ln-tOw"/> + </constraints> + <point key="canvasLocation" x="140" y="96.059113300492612"/> + </view> + </objects> + <resources> + <image name="minus" catalog="system" width="128" height="24"/> + <image name="plus" catalog="system" width="128" height="113"/> + <systemColor name="labelColor"> + <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </systemColor> + <systemColor name="secondarySystemBackgroundColor"> + <color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + </systemColor> + <systemColor name="systemBackgroundColor"> + <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </systemColor> + </resources> +</document> diff --git a/iosApp/iosApp/Map/MapWrapperView.swift b/iosApp/iosApp/Map/MapWrapperView.swift deleted file mode 100644 index 7450eed..0000000 --- a/iosApp/iosApp/Map/MapWrapperView.swift +++ /dev/null @@ -1,87 +0,0 @@ -/** - * TrackerMap - * Copyright (C) 2021-2022 Iván Ávalos <avalos@disroot.org>, Henoch Ojeda <imhenoch@protonmail.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -import SwiftUI -import Combine -import WhirlyGlobeMaplyComponent -import shared - -struct MapWrapperView: View { - @Binding var layer: MapLayer - @Binding var markers: [Marker] - var markerCallback: ((Int32?) -> Void)? - @ObservedObject var link: BaseMapLink = BaseMapLink() - - var body: some View { - ZStack { - // MARK: - Map - MapView(mapLayer: $layer, - markers: $markers, - markerCallback: markerCallback, - link: link) - - // MARK: - Attribution - VStack { - HyperlinkText(html: layer.attribution) - .font(.footnote) - .lineLimit(3) - .foregroundColor(.label.opacity(0.35)) - .padding(6.0) - .background(.systemBackground.opacity(0.35)) - } - .frame(maxWidth: .infinity, - maxHeight: .infinity, - alignment: .bottom) - .allowsHitTesting(false) - - // MARK: - Controls - VStack { - // MARK: Zoom in - Button { - print ("Zoom in!") - link.zoomIn() - } label: { - Image(systemName: "plus").imageScale(.large) - } - .buttonStyle(ControlButtonStyle()) - - // MARK: Zoom out - Button { - print("Zoom out!") - link.zoomOut() - } label: { - Image(systemName: "minus").imageScale(.large) - } - .buttonStyle(ControlButtonStyle()) - } - .frame(maxWidth: .infinity, - maxHeight: .infinity, - alignment: .topTrailing) - .padding() - } - } -} - -struct ControlButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .frame(width: 50, height: 50) - .foregroundColor(configuration.isPressed ? .secondary : .primary) - .background(configuration.isPressed ? .secondarySystemBackground : .systemBackground) - .clipShape(Circle()) - } -} diff --git a/iosApp/iosApp/Map/UnitMapView.swift b/iosApp/iosApp/Map/UnitMapView.swift index e1f90a0..5f418f1 100644 --- a/iosApp/iosApp/Map/UnitMapView.swift +++ b/iosApp/iosApp/Map/UnitMapView.swift @@ -23,9 +23,9 @@ struct UnitMapView: View { var body: some View { ZStack { - MapWrapperView(layer: $unitsViewModel.mapLayerType, - markers: $unitsViewModel.markers, - markerCallback: unitsViewModel.selectUnitWith) + MapView(layer: $unitsViewModel.mapLayerType, + markers: $unitsViewModel.markers, + markerCallback: unitsViewModel.selectUnitWith) if let unit = unitsViewModel.selectedUnit { VStack { DeviceRow(unit: unit, callback: { action in diff --git a/iosApp/iosApp/Shared/HyperlinkText.swift b/iosApp/iosApp/Shared/HyperlinkText.swift index 69c758b..a735f8d 100644 --- a/iosApp/iosApp/Shared/HyperlinkText.swift +++ b/iosApp/iosApp/Shared/HyperlinkText.swift @@ -15,129 +15,89 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import SwiftUI -import SwiftUIFlowLayout +import Foundation +import UIKit -// Source: https://swiftuirecipes.com/blog/hyperlinks-in-swiftui-text -struct HyperlinkText: View { - private let pairs: [StringWithAttributes] - - init(_ attributedString: NSAttributedString) { - pairs = attributedString.stringsWithAttributes +// Source: https://stackoverflow.com/a/50272137 +extension UIColor { + enum HexFormat { + case RGB + case ARGB + case RGBA + case RRGGBB + case AARRGGBB + case RRGGBBAA } - - init?(html: String) { - if let data = html.data(using: .utf8), - let attributedString = try? NSAttributedString(data: data, - options: [.documentType: NSAttributedString.DocumentType.html], - documentAttributes: nil) { - self.init(attributedString) - } else { - return nil - } - } - - var body: some View { - FlowLayout(mode: .vstack, - binding: .constant(false), - items: pairs, - itemSpacing: 0) { pair in - if let link = pair.attrs[.link], - let url = link as? URL { - Text(pair) - .onTapGesture { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } - } else { - Text(pair) - } - } - } -} -struct StringWithAttributes: Hashable, Identifiable { - let id = UUID() - let string: String - let attrs: [NSAttributedString.Key: Any] - - static func == (lhs: StringWithAttributes, rhs: StringWithAttributes) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) + enum HexDigits { + case d3, d4, d6, d8 } -} -extension NSAttributedString { - var stringsWithAttributes: [StringWithAttributes] { - var attributes = [StringWithAttributes]() - enumerateAttributes(in: NSRange(location: 0, length: length), options: []) { (attrs, range, _) in - let string = attributedSubstring(from: range).string - attributes.append(StringWithAttributes(string: string, attrs: attrs)) - } - return attributes - } -} + func hexString(_ format: HexFormat = .RRGGBBAA) -> String { + let maxi = [.RGB, .ARGB, .RGBA].contains(format) ? 16 : 256 -extension Text { - init(_ singleAttribute: StringWithAttributes) { - let string = singleAttribute.string - let attrs = singleAttribute.attrs - var text = Text(string) - - /*if let font = attrs[.font] as? UIFont { - text = text.font(.init(font)) - } - - if let color = attrs[.foregroundColor] as? UIColor { - text = text.foregroundColor(Color(color)) - }*/ - - if let kern = attrs[.kern] as? CGFloat { - text = text.kerning(kern) - } - - if #available(iOS 14.0, *) { - if let tracking = attrs[.tracking] as? CGFloat { - text = text.tracking(tracking) - } + func toI(_ f: CGFloat) -> Int { + return min(maxi - 1, Int(CGFloat(maxi) * f)) } - - if let strikethroughStyle = attrs[.strikethroughStyle] as? NSNumber, strikethroughStyle != 0 { - if let strikethroughColor = (attrs[.strikethroughColor] as? UIColor) { - text = text.strikethrough(true, color: Color(strikethroughColor)) - } else { - text = text.strikethrough(true) - } - } - - if let underlineStyle = attrs[.underlineStyle] as? NSNumber, - underlineStyle != 0 { - if let underlineColor = (attrs[.underlineColor] as? UIColor) { - text = text.underline(true, color: Color(underlineColor)) - } else { - text = text.underline(true) - } - } - - if let baselineOffset = attrs[.baselineOffset] as? NSNumber { - text = text.baselineOffset(CGFloat(baselineOffset.floatValue)) + + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + self.getRed(&r, green: &g, blue: &b, alpha: &a) + + let ri = toI(r) + let gi = toI(g) + let bi = toI(b) + let ai = toI(a) + + switch format { + case .RGB: return String(format: "#%X%X%X", ri, gi, bi) + case .ARGB: return String(format: "#%X%X%X%X", ai, ri, gi, bi) + case .RGBA: return String(format: "#%X%X%X%X", ri, gi, bi, ai) + case .RRGGBB: return String(format: "#%02X%02X%02X", ri, gi, bi) + case .AARRGGBB: return String(format: "#%02X%02X%02X%02X", ai, ri, gi, bi) + case .RRGGBBAA: return String(format: "#%02X%02X%02X%02X", ri, gi, bi, ai) } - - self = text } - - init(_ attributes: [StringWithAttributes]) { - self.init("") - for singleAttribute in attributes { - self = self + Text(singleAttribute) + + func hexString(_ digits: HexDigits) -> String { + switch digits { + case .d3: return hexString(.RGB) + case .d4: return hexString(.RGBA) + case .d6: return hexString(.RRGGBB) + case .d8: return hexString(.RRGGBBAA) } } - - init(_ attributedString: NSAttributedString) { - self.init(attributedString.stringsWithAttributes) +} + +// Source: https://swiftuirecipes.com/blog/swiftui-text-with-html-via-nsattributedstring +class HtmlString { + static func htmlToAttrStr(_ html: String, size: CGFloat, color: UIColor) -> NSAttributedString? { + let fullHTML = """ + <!doctype html> + <html> + <head> + <style> + body { + font-family: -apple-system; + font-size: \(size)px; + color: \(color.hexString(.RGB)) + } + </style> + </head> + <body> + \(html) + </body> + </html> + """ + if let data = fullHTML.data(using: .utf8) { + return try? NSAttributedString( + data: data, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil) + } + return nil } } |