From 30b87deb7e98a0b1574f1a9ec7b2fba14a7fe5d0 Mon Sep 17 00:00:00 2001 From: Alex Sherstnev Date: Sun, 18 Jan 2026 01:22:50 +0100 Subject: [PATCH] issue #54: Adjust zoom levels to screen size --- Rewind/MapView/MapAdapter.swift | 4 ++ Rewind/Model/CameraSession.swift | 4 +- Rewind/Model/Data Types/ModelImage.swift | 2 + Rewind/Model/LocalClustering.swift | 14 ++++--- Rewind/Model/MapModel.swift | 21 ++++++++--- Rewind/Model/RewindRemotes.swift | 21 +++++++++-- Rewind/Model/Zoom.swift | 47 +++++++++++------------- Rewind/PastvuColors.swift | 2 +- Rewind/View/MapControlsHiding.swift | 2 +- 9 files changed, 74 insertions(+), 43 deletions(-) diff --git a/Rewind/MapView/MapAdapter.swift b/Rewind/MapView/MapAdapter.swift index 173c78e..37e5862 100644 --- a/Rewind/MapView/MapAdapter.swift +++ b/Rewind/MapView/MapAdapter.swift @@ -19,6 +19,10 @@ final class MapAdapter: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate private var pipe = SignalPipe() private var showYearColorInClusters: ObservableVariable + var size: CGSize { + map.value.frame.size + } + var view: UIView { map.value } diff --git a/Rewind/Model/CameraSession.swift b/Rewind/Model/CameraSession.swift index 5b0311b..e79b000 100644 --- a/Rewind/Model/CameraSession.swift +++ b/Rewind/Model/CameraSession.swift @@ -66,8 +66,8 @@ final class CameraSession { } func setLens(lens: Lens, animated: Bool) throws { - let clamped = lens.zoomValue.clamped( - in: device.minAvailableVideoZoomFactor...device.maxAvailableVideoZoomFactor + let clamped = lens.zoomValue.clamp( + device.minAvailableVideoZoomFactor...device.maxAvailableVideoZoomFactor ) try device.lockForConfiguration() defer { device.unlockForConfiguration() } diff --git a/Rewind/Model/Data Types/ModelImage.swift b/Rewind/Model/Data Types/ModelImage.swift index be94d43..d9cc653 100644 --- a/Rewind/Model/Data Types/ModelImage.swift +++ b/Rewind/Model/Data Types/ModelImage.swift @@ -7,6 +7,8 @@ import UIKit +enum Model {} // namespace only + extension Model { struct Image { var cid: Int diff --git a/Rewind/Model/LocalClustering.swift b/Rewind/Model/LocalClustering.swift index 5c7b1ea..bb6539b 100644 --- a/Rewind/Model/LocalClustering.swift +++ b/Rewind/Model/LocalClustering.swift @@ -21,13 +21,14 @@ func makeDiffAfterReceived( images: [Model.Image], clusters: [Model.Cluster], params: AnnotationLoadingParams, + mapSize: CGSize, state: inout MapState ) -> ( toAdd: [AnnotationValue], toRemove: [AnnotationValue] ) { let shouldClearOldAnnotations = if let lastParams = state.lastLoadedParams { - lastParams.region.zoom != params.region.zoom + lastParams.zoom != params.zoom || lastParams.yearRange != params.yearRange } else { false @@ -70,13 +71,15 @@ func makeDiffAfterReceived( toRemove += staleImages.map { .image($0) } state.clusteredImages = groupImages( images: freeImages.intersection(receivedImages), - zoom: params.region.zoom + zoom: params.zoom, + mapSize: mapSize ).map(key: { $0 }, value: { .left($0) }) } let groupedImages = groupImages( images: receivedImages, - zoom: params.region.zoom + zoom: params.zoom, + mapSize: mapSize ) let patches = makePatches( newImages: groupedImages, @@ -93,9 +96,10 @@ func makeDiffAfterReceived( private func groupImages( images: any Sequence, - zoom: Int + zoom: Int, + mapSize: CGSize ) -> [ClusteringCell: Set] { - let size = delta(zoom: zoom) / clusteringCellRatio + let size = delta(zoom: zoom, mapSize: mapSize) / clusteringCellRatio return images.reduce(into: [:]) { result, image in let cell = ClusteringCell( latIndex: Int(floor(image.coordinate.latitude / size)), diff --git a/Rewind/Model/MapModel.swift b/Rewind/Model/MapModel.swift index 0e821a7..80bc402 100644 --- a/Rewind/Model/MapModel.swift +++ b/Rewind/Model/MapModel.swift @@ -96,8 +96,14 @@ func makeMapModel( if settings.value.openClusterPreviews { performAppAction(.imageDetails(.present(cluster.preview, source: "annotation"))) } else { + let mapSize = mapAdapter.size + let currentZoom = Rewind.zoom(region: state.region, mapSize: mapSize) mapAdapter.set( - region: Region(center: cluster.coordinate, zoom: state.region.zoom + 1), + region: Region( + center: cluster.coordinate, + zoom: currentZoom + 1, + mapSize: mapSize + ), animated: true ) } @@ -150,7 +156,7 @@ func makeMapModel( case .locationButtonTapped: if let location = state.locationState.location { mapAdapter.set( - region: Region(center: location.coordinate, zoom: 15), + region: Region(center: location.coordinate, zoom: 17, mapSize: mapAdapter.size), animated: true ) } else if state.locationState.isAccessGranted == false { @@ -167,13 +173,13 @@ func makeMapModel( mapAdapter.deselectAnnotations() case let .focusOn(coordinate, zoom): mapAdapter.set( - region: Region(center: coordinate, zoom: zoom), + region: Region(center: coordinate, zoom: zoom, mapSize: mapAdapter.size), animated: true ) case let .newLocationState(locationState): if let location = locationState.location, state.locationState.location == nil { mapAdapter.set( - region: Region(center: location.coordinate, zoom: 15), + region: Region(center: location.coordinate, zoom: 15, mapSize: mapAdapter.size), animated: false ) } @@ -192,7 +198,11 @@ func makeMapModel( )) case .loadAnnotations: state.isLoading = true - let params = AnnotationLoadingParams(region: state.region, yearRange: state.yearRange) + let params = AnnotationLoadingParams( + region: state.region, + yearRange: state.yearRange, + mapSize: mapAdapter.size + ) enqueueEffect(.perform(id: EffectID.loadAnnotations) { anotherAction in do { let (images, clusters) = try await annotationsRemote.load(params) @@ -212,6 +222,7 @@ func makeMapModel( images: images, clusters: clusters, params: params, + mapSize: mapAdapter.size, state: &state ) state.isLoading = false diff --git a/Rewind/Model/RewindRemotes.swift b/Rewind/Model/RewindRemotes.swift index 9426d59..e88414b 100644 --- a/Rewind/Model/RewindRemotes.swift +++ b/Rewind/Model/RewindRemotes.swift @@ -14,8 +14,21 @@ struct RewindRemotes { } struct AnnotationLoadingParams { - var region: Region + var zoom: Int + var coordinates: [[Double]] + var startAt: TimeInterval var yearRange: ClosedRange + + init( + region: Region, + yearRange: ClosedRange, + mapSize: CGSize + ) { + self.zoom = Rewind.zoom(region: region, mapSize: mapSize) + self.coordinates = region.geoJSONCoordinates + self.startAt = Date().timeIntervalSince1970 + self.yearRange = yearRange + } } extension RewindRemotes { @@ -26,9 +39,9 @@ extension RewindRemotes { annotations = Remote { params in let (nis, ncs) = try await requestPerformer.perform( request: .byBounds( - zoom: params.region.zoom, - coordinates: params.region.geoJSONCoordinates, - startAt: Date().timeIntervalSince1970, + zoom: params.zoom, + coordinates: params.coordinates, + startAt: params.startAt, yearRange: params.yearRange ) ) diff --git a/Rewind/Model/Zoom.swift b/Rewind/Model/Zoom.swift index 56263f2..1168991 100644 --- a/Rewind/Model/Zoom.swift +++ b/Rewind/Model/Zoom.swift @@ -6,26 +6,35 @@ // import MapKit - -enum Model {} // namespace only +import VGSL // https://leafletjs.com/examples/zoom-levels/ -private func zoom(delta: Double) -> Int { - Int((2 + log2(180 / delta)).rounded(.toNearestOrAwayFromZero)) +func zoom(region: Region, mapSize: CGSize) -> Int { + let delta = min(region.span.latitudeDelta, region.span.longitudeDelta) + return Int( + log2(360 / delta).rounded(.toNearestOrAwayFromZero) + + adjustment(mapSize: mapSize) + ).clamp(3...19) } -func delta(zoom: Int) -> Double { - 180 / pow(2, Double(zoom - 2)) +func delta(zoom: Int, mapSize: CGSize) -> Double { + 360 / pow(2, Double(zoom) - adjustment(mapSize: mapSize)) } -extension Region { - var zoom: Int { - let delta = min(span.latitudeDelta, span.longitudeDelta) - return Rewind.zoom(delta: delta).clamped(in: minZoom...maxZoom) - } +private func adjustment(mapSize: CGSize) -> Double { + let x = min(mapSize.width, mapSize.height) + let (x1, y1) = (375.0, 0.7) // min adjustment for small screens + let (x2, y2) = (1024.0, 1.5) // max adjustment for large screens + return (y2 - y1) / (x2 - x1) * (x - x1) + y1 +} - init(center: Coordinate, zoom: Int) { - let delta = delta(zoom: zoom) +extension Region { + init( + center: Coordinate, + zoom: Int, + mapSize: CGSize + ) { + let delta = delta(zoom: zoom, mapSize: mapSize) self.init( center: center, span: MKCoordinateSpan( @@ -35,15 +44,3 @@ extension Region { ) } } - -extension Comparable { - func clamped(in range: ClosedRange) -> Self { - let lowerBound = range.lowerBound - let upperBound = range.upperBound - - return min(upperBound, max(lowerBound, self)) - } -} - -private let minZoom = 3 -private let maxZoom = 19 diff --git a/Rewind/PastvuColors.swift b/Rewind/PastvuColors.swift index be83f38..2dbff01 100644 --- a/Rewind/PastvuColors.swift +++ b/Rewind/PastvuColors.swift @@ -53,7 +53,7 @@ extension RGBAColor { extension Gradient { fileprivate func color(at rawT: CGFloat) -> RGBAColor { - let t = rawT.clamped(in: 0...1) + let t = rawT.clamp(0...1) guard let index = binSearch(firstEqualOrGreaterThan: t, keyPath: \.position, in: self) else { return self.last!.color } diff --git a/Rewind/View/MapControlsHiding.swift b/Rewind/View/MapControlsHiding.swift index 3488180..24796ec 100644 --- a/Rewind/View/MapControlsHiding.swift +++ b/Rewind/View/MapControlsHiding.swift @@ -104,7 +104,7 @@ private struct MinimizableContainer: ViewModifier { .updating($pulling) { value, pulling, _ in if value.isVertical, state.isNormal { - pulling.progress = (value.translation.height / -minPullLength).clamped(in: 0...1) + pulling.progress = (value.translation.height / -minPullLength).clamp(0...1) if pulling.progress.isApproximatelyEqualTo(1), !pulling.handled { onPull() pulling.handled = true