孩子喜欢飞机,于是我给她做了一个雷达
2023-11-28 17:12:13 软件 219观看
摘要今年夏天,我计划带着我的孩子出国。她很兴奋。在此之前,我和妻子决定大肆宣传一下这次的飞行之旅,主要是为了确保女儿能安稳地度过3小时的飞行时间。可能是我们宣传有点过头了,以至于当我们不得不坐出租车去机场时,我蹒跚

今年夏天,我计划带着我的孩子出国。pJz28资讯网——每日最新资讯28at.com

她很兴奋。pJz28资讯网——每日最新资讯28at.com

在此之前,我和妻子决定大肆宣传一下这次的飞行之旅,主要是为了确保女儿能安稳地度过3小时的飞行时间。pJz28资讯网——每日最新资讯28at.com

可能是我们宣传有点过头了,以至于当我们不得不坐出租车去机场时,我蹒跚学步的孩子感到震惊——她原本以为会从我们家直接走上飞机。pJz28资讯网——每日最新资讯28at.com

我们登机后,发生了一件令人难以置信的事情。pJz28资讯网——每日最新资讯28at.com

原来,当机组人员发现你和一个痴迷于飞机的可爱小孩在一起时,他们会邀请你们去看看驾驶舱。pJz28资讯网——每日最新资讯28at.com

这激发了我女儿对飞机的痴迷。pJz28资讯网——每日最新资讯28at.com

从那之后,她一直要求我在天上为她寻找飞机,当我为她找到一架飞机时,她很高兴。pJz28资讯网——每日最新资讯28at.com

上周,我们在花园里待了一个小时,她坐在我的肩上,看着飞机一架接一架地在夜空中闪烁。pJz28资讯网——每日最新资讯28at.com

后来我找到了FlightRadar24,它能显示覆盖在地图上的飞机位置,但美中不足的是,我必须自己调整方向。pJz28资讯网——每日最新资讯28at.com

图片pJz28资讯网——每日最新资讯28at.com

但是,对于一个孩子来说,她可能并不真正理解或关心地图是什么。pJz28资讯网——每日最新资讯28at.com

所以我们有了继续解决的新问题,比如方向,比如可用性。pJz28资讯网——每日最新资讯28at.com

作为一名非物理移动技术主管,我确实不知道从哪里开始为孩子打造一匹摇马,但没有什么能阻止我把这个想法变成一个很酷的应用程序。pJz28资讯网——每日最新资讯28at.com

在雷达上显示附近的航班

通过研究制定的要求:pJz28资讯网——每日最新资讯28at.com

  • 该应用程序需要保持正确的方向,随设备旋转,以便显示飞机的正确方向。
  • 该应用程序必须根据飞机的高度将飞机图标显示为更大或更小。
  • 该应用程序必须很有趣,要有一种复古儿童玩具的感觉,而不是严肃的商业应用程序。

这些要求导致了一些构成概念验证的活动部分:pJz28资讯网——每日最新资讯28at.com

  • 保持方向是差异化产品的核心要求,因为现有解决方案缺少这一点。我不关心详细的航班信息,我只是想制作一个很酷的雷达。iOS 核心位置API已被涵盖,每次用户重新调整设备方向时都会提供委托回调。
  • 最重要的组件是Flight Data API。OpenSky Network正是我所需要的。一个简单的REST API,免费供非商业用途,包含某个区域的航班实时数据。我们希望每隔几秒就对这个端点执行操作,以进行真实的雷达扫描。
  • 为了调用 API,还需要一些位置数据。Core Location可供查询距用户位置+/-1度的纬度,精度为0.1度(约10公里),以确保用户的位置足够模糊。我们也只需要在每个会话中获取一次该数据。
  • 最后,我们需要重新掌握三角学技能,将飞行位置数据与我们自己的定向坐标进行比较。这将使我们能够根据附近的飞机在天空中与我们的相对位置,将其绘制到屏幕上的正确位置。

概念验证

对于图标,我选择了一幅女儿戴着可爱飞行员帽的卡通画。所以我们已经有了应用程序名称:Aviator。pJz28资讯网——每日最新资讯28at.com

方向

第一个关键差异化产品要求是保持方向。pJz28资讯网——每日最新资讯28at.com

为了使用便利,屏幕上的对象需要与其现实生活中的位置相对应。因此,当用户旋转时,屏幕本身也会旋转并保持指向北。pJz28资讯网——每日最新资讯28at.com

final class LocationManager: CLLocationManager, CLLocationManagerDelegate {            static let shared = LocationManager()        private(set) var rotationAngleSubject = CurrentValueSubject<Double, Never>(0)        override private init() {        super.init()        requestWhenInUseAuthorization()        delegate = self        startUpdatingHeading()    }        func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {        rotationAngleSubject.send(-newHeading.magneticHeading)    }}

同时,为了获得好看的指南针效果,我还绘制了一组随旋转角度变化的矩形。pJz28资讯网——每日最新资讯28at.com

@State private var rotationAngle: Angle = .degrees(0)var body: some View {    ZStack {        ForEach(0..<36) {            let angle = Angle.degrees(Double($0 * 10)) + rotationAngle            Rectangle()                .frame(width: $0 == 0 ? 16 : 8, height: $0 == 0 ? 3 : 2)                .foregroundColor($0 == 0 ? .red : .blue)                .rotationEffect(angle)                .offset(x: 120 * cos(CGFloat(angle.radians)), y: 120 * sin(CGFloat(angle.radians)))                .animation(.bouncy, value: rotationAngle)        }    }    .onReceive(LocationManager.shared.rotationAngleSubject) { angle in        rotationAngle = Angle.degrees(angle)    }}

看起来相当不错,而且也完美地响应了我的真实位置。pJz28资讯网——每日最新资讯28at.com

图片pJz28资讯网——每日最新资讯28at.com

可能你会注意到一个有趣的视觉故障,因为动画逻辑将0度和360度视为单独的数字——当我经过正北时,所有矩形都会旋转。pJz28资讯网——每日最新资讯28at.com

航班数据

热身结束,接下来是重要的部分。pJz28资讯网——每日最新资讯28at.com

OpenSky Network API允许用户给定一系列纬度和经度,通过一个简单的请求返回该范围内的本地航班数组。这意味着,只需将其粘贴到浏览器中,即可找出我可以看到的头顶上空的航班数据。pJz28资讯网——每日最新资讯28at.com

REST API记录良好,但数据按顺序显示为列表属性。pJz28资讯网——每日最新资讯28at.com

图片pJz28资讯网——每日最新资讯28at.com

我们需要去解码它,让其按顺序从JSON响应中解析出字段。pJz28资讯网——每日最新资讯28at.com

struct Flight: Decodable {    let icao24: String     let callsign: String?    let origin_country: String?     let time_position: Int?    let last_contact: Int    let longitude: Double    let latitude: Double    // ...     init(from decoder: Decoder) throws {        var container = try decoder.unkeyedContainer()        icao24 = try container.decode(String.self)        callsign = try? container.decode(String?.self)        origin_country = try container.decode(String.self)        time_position = try? container.decode(Int?.self)        last_contact = try container.decode(Int.self)        longitude = try container.decode(Double.self)        latitude = try container.decode(Double.self)        // ...    }}

我们还可以编写一个简单的API,根据用户的位置坐标执行请求。pJz28资讯网——每日最新资讯28at.com

final class FlightAPI {        func fetchLocalFlightData(coordinate: CLLocationCoordinate2D) async throws -> [Flight] {                let lamin = String(format: "%.1f", coordinate.latitude - 0.25)        let lamax = String(format: "%.1f", coordinate.latitude + 0.25)        let lomin = String(format: "%.1f", coordinate.longitude - 0.5)        let lomax = String(format: "%.1f", coordinate.longitude + 0.5)        let url = URL(string: "https://opensky-network.org/api/states/all?lamin=/(lamin)&lamax=/(lamax)&lomin=/(lomin)&lomax=/(lomax)")!        let data = try await URLSession.shared.data(from: url).0        return try JSONDecoder().decode([Flight].self, from: data)    }}

这样飞行数据就被很好地解析为内存中对象的数组,也变得易于处理。pJz28资讯网——每日最新资讯28at.com

初步结果

如何实际测试飞机图纸的准确性?pJz28资讯网——每日最新资讯28at.com

我们可以在这些所有东西下面画一张地图:AviatorView顶部的指南针,绘制到屏幕上的飞机,以及朴素的SwiftUI视图。pJz28资讯网——每日最新资讯28at.com

@State private var cameraPosition: MapCameraPosition = .camera(MapCamera(        centerCoordinate: CLLocationCoordinate2D(latitude: 51.0, longitude: 0.0),        distance: 100_000,        heading: 0))var body: some View {    ZStack {        Map(position: $cameraPosition) { }         airplanes        compass    }}

这是我第一次熬夜跑出来的结果,与作为事实来源的FlightRadar进行比较。pJz28资讯网——每日最新资讯28at.com

图片pJz28资讯网——每日最新资讯28at.com

可以看到,天空中飞机的数量和集群看起来都差不多,但位置却相差甚远。忽然,我灵光一闪,原来还需要使用注释在地图上绘制飞机。pJz28资讯网——每日最新资讯28at.com

MVP

这个想法我已经酝酿了一整天:我们使用地图,然后在其精确地理位置的顶部绘制飞机形状的注释,最终,我想找到一种方法来隐藏实际地图,并仅将飞机显示为雷达位置上的标记。pJz28资讯网——每日最新资讯28at.com

这应该会给我们带来我们想要的很酷的、完全定向的雷达效果。pJz28资讯网——每日最新资讯28at.com

地图注释

在iOS 17中,在地图上绘制注释非常简单。pJz28资讯网——每日最新资讯28at.com

import MapKitimport SwiftUIstruct FlightMapView: View {        @Binding var cameraPosition: MapCameraPosition        let flights: [Flight]    var body: some View {        Map(position: $cameraPosition) {            planeMapAnnotations        }        .mapStyle(.imagery)        .allowsHitTesting(false)    }}

在这里,出于雷达的目的,我们希望防止命中测试——即我不希望地图是交互式的。在构想中,地图是不可见的,用户只能看到航班及其位置。pJz28资讯网——每日最新资讯28at.com

飞机缩放

定位之后,尺寸调整是下一个核心问题,现有的解决方案根本无法很好地处理这个问题。pJz28资讯网——每日最新资讯28at.com

我使用飞行高度在地图注释中添加了一些简单的对数缩放,以便更高的飞机在屏幕上显得更大。此外,我使用飞机的真实属性,结合核心位置中的用户方向,来显示飞机面向正确的方向。pJz28资讯网——每日最新资讯28at.com

@State private var rotationAngle: Angle = .degrees(0)private var planeMapAnnotations: some MapContent {    ForEach(flights, id: /.icao24) { flight in        Annotation(flight.icao24, coordinate: flight.coordinate) {            let rotation = rotationAngle.degrees + flight.true_track            let scale = min(2, max(log10(height + 1), 0.5))            Image(systemName: "airplane")                .rotationEffect(.degrees(rotation))                .scaleEffect(scale)            }        }        .tint(.white)    }}

用户调研

现在是进行终极测试的时候了。pJz28资讯网——每日最新资讯28at.com

我和女儿一起去看飞机,现在我们有了真实的地图注释,能在地图上显示用户的位置和方向。最重要的是,它能够准确地找到飞机!pJz28资讯网——每日最新资讯28at.com

图片pJz28资讯网——每日最新资讯28at.com

这获得了巨大成功,因为我们在这上面找到了飞机。pJz28资讯网——每日最新资讯28at.com

初步测试还得出了两条重要信息。pJz28资讯网——每日最新资讯28at.com

首先,缩放逻辑是不正确的。看看伦敦城市机场地面上的小飞机。由于应用程序的重点是定位天空中的飞机,因此我们需要反转缩放比例,较低的平面必须显示得更大,因为我们是用眼睛来发现它们的。pJz28资讯网——每日最新资讯28at.com

其次,我的孩子不关心地图,只关心飞机。如果我想消除噪音并专注于发现飞机,我需要删除地图,并开始建造我的雷达!pJz28资讯网——每日最新资讯28at.com

更新缩放逻辑

我轻松地修复了飞机的缩放逻辑。pJz28资讯网——每日最新资讯28at.com

经过一番尝试和错误后,为了查看屏幕上看起来不错的内容,并给出合理的尺寸分布,我选择了缩放:pJz28资讯网——每日最新资讯28at.com

min(2, max(4.7 - log10(flight.geo_altitude + 1), 0.7))

这些缩放来自我的本地开销扫描:pJz28资讯网——每日最新资讯28at.com

Scale:  1.0835408863965839Scale:  0.8330645861650874Scale:  1.095791123396205Scale:  1.1077242935783653Scale:  2.0Scale:  1.4864702267977097Scale:  0.7

创建雷达

我几乎准备好建造我所设想的雷达了,但是出现了一个问题。pJz28资讯网——每日最新资讯28at.com

API稳健性

开源OpenSky API不断超时,返回502错误,或者有时生成带有空数据的200响应。pJz28资讯网——每日最新资讯28at.com

这其实也不是问题,毕竟这不是个企业级应用程序,而且这个API不需要我花任何费用。他们没有SLA,我也觉得自己没有资格获得SLA。不过为了帮助提高客户端的稳健性,我在API调用中实现了一些基本的重试逻辑:pJz28资讯网——每日最新资讯28at.com

private func fetchFlights(at coordinate: CLLocationCoordinate2D, retries: Int = 3) async {    do {        try await api.fetchLocalFlightData(coordinate: coordinate)    } catch {        if retries > 0 {            try await fetchFlights(at: coordinate, retries: retries - 1)        }    }}

第二天,API运行良好,除了某些高流量时刻外。pJz28资讯网——每日最新资讯28at.com

覆盖地图

最重要的降噪任务是使实际地图不可见。没有这个雷达就无法工作。pJz28资讯网——每日最新资讯28at.com

我能够使用MapPolygon来做到这一点,表面上设计这样你就可以放置叠加层来突出显示地图的各个部分。但我想用它来隐藏除注释之外的所有内容。pJz28资讯网——每日最新资讯28at.com

struct FlightMapView: View {    var body: some View {        Map(position: $cameraPosition) {            planeMapAnnotations            MapPolygon(overlay(coordinate: coordinate))        }        .mapStyle(.imagery)        .allowsHitTesting(false)    }    // ...       private func rectangle(around coordinate: CLLocationCoordinate2D) -> [CLLocationCoordinate2D] {        [            CLLocationCoordinate2D(latitude: coordinate.latitude - 1, longitude: coordinate.longitude - 1),            CLLocationCoordinate2D(latitude: coordinate.latitude - 1, longitude: coordinate.longitude + 1),            CLLocationCoordinate2D(latitude: coordinate.latitude + 1, longitude: coordinate.longitude + 1),            CLLocationCoordinate2D(latitude: coordinate.latitude + 1, longitude: coordinate.longitude - 1)        ]    }        private func overlay(coordinate: CLLocationCoordinate2D) -> MKPolygon {        let rectangle = rectangle(around: coordinate)        return MKPolygon(coordinates: rectangle, count: rectangle.count)    }}

这种方法很有效!pJz28资讯网——每日最新资讯28at.com

我们现在可以看到飞机,但看不到地图,就像我们想要的那样。pJz28资讯网——每日最新资讯28at.com

最关键的是,苹果将叠加层设计为位于地图顶部、注释下方,如果他们采取其他方式,我女儿的新玩具就会跛行。pJz28资讯网——每日最新资讯28at.com

绘制雷达

核心需求的最后一部分是雷达视图,这本质上是一组直线、同心圆和20度的旋转角梯度。pJz28资讯网——每日最新资讯28at.com

难不倒我。pJz28资讯网——每日最新资讯28at.com

用户调研2

经过三个晚上的辛苦工作,女儿终于开始对我创造的玩具表现出一些兴趣。pJz28资讯网——每日最新资讯28at.com

图片pJz28资讯网——每日最新资讯28at.com

我们已经证明了这个概念,并构建了一个 MVP,可以实现我们设定的核心初始目标。pJz28资讯网——每日最新资讯28at.com

现在可以考虑把它放到App Store上了。pJz28资讯网——每日最新资讯28at.com

当然在此之前还需要进行其他的优化。pJz28资讯网——每日最新资讯28at.com

比如让雷达有360度宽角渐变,从绿色,到透明,到透明,到透明,再到黑色。pJz28资讯网——每日最新资讯28at.com

private var radarLine: some View {    Circle()        .fill(            AngularGradient(                gradient: Gradient(colors: [                    Color.black, Color.black, Color.black, Color.black,                    Color.black.opacity(0.8), Color.black.opacity(0.6),                    Color.black.opacity(0.4), Color.black.opacity(0.2),                    Color.clear, Color.clear, Color.clear, Color.clear,                    Color.clear, Color.clear, Color.clear, Color.clear,                    Color.clear, Color.clear, Color.clear, Color.green]),                center: .center,                startAngle: .degrees(rotationDegree),                endAngle: .degrees(rotationDegree + 360)            )        )        .rotationEffect(Angle(degrees: rotationDegree))        .animation(.linear(duration: 6).repeatForever(autoreverses: false), value: rotationDegree)}

除此之外,我添加了CRT屏幕效果和电视扫描线,使应用程序看起来就像是在旧雷达扫描仪上绘制的。pJz28资讯网——每日最新资讯28at.com

#include <metal_stdlib>using namespace metal;[[ stitchable ]] half4 crtScreen(    float2 position,    half4 color,    float time) {        if (all(abs(color.rgb - half3(0.0, 0.0, 0.0)) < half3(0.01, 0.01, 0.01))) {        return color;    }        const half scanlineIntensity = 0.2;    const half scanlineFrequency = 400.0;    half scanlineValue = sin((position.y + time * 10.0) * scanlineFrequency * 3.14159h) * scanlineIntensity;    return half4(color.rgb - scanlineValue, color.a);}

我还创建了一个视图修改器,可以将CRT效果应用到喜欢的任何视图。pJz28资讯网——每日最新资讯28at.com

extension View {        func crtScreenEffect(startTime: Date) -> some View {        modifier(CRTScreen(startTime: startTime))    }}struct CRTScreen: ViewModifier {        let startTime: Date        func body(content: Content) -> some View {        content            .colorEffect(                ShaderLibrary.crtScreen(                    .float(startTime.timeIntervalSinceNow)                )            )    }}

图片pJz28资讯网——每日最新资讯28at.com

目前该应用程序已经上线了App Store。pJz28资讯网——每日最新资讯28at.com


pJz28资讯网——每日最新资讯28at.com

图片pJz28资讯网——每日最新资讯28at.com

同时下个版本的新功能也已经在构想中了,包括但不限于:pJz28资讯网——每日最新资讯28at.com

  • 向地图添加缩放级别,以将雷达限制为仅检测较近的飞机。
  • 使用OpenSky Network API的高级版本显示直升机、卫星和飞机尺寸类别。
  • 切换飞机上的出发地和目的地国家/地区显示。
  • 使用更先进的金属着色器改善CRT屏幕效果。
  • 实施滑块控件来过滤掉某些距离和高度,例如隐藏所有低矮、遥远的飞机。
  • 实施“滑稽模式”,在雷达上呈现不明飞行物、巨型虫子和外星人。

本文链接:http://www.28at.com/showinfo-26-34927-0.html孩子喜欢飞机,于是我给她做了一个雷达

声明:本网页内容旨在传播知识,不代表本站观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。

显示全文

上一篇:C++数据与量值是如何被组织的?

下一篇:理解C++之构造函数

最新热点