自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

孩子喜歡飛機(jī),于是我給她做了一個(gè)雷達(dá)

開發(fā) 后端
作為一名非物理移動(dòng)技術(shù)主管,我確實(shí)不知道從哪里開始為孩子打造一匹搖馬,但沒有什么能阻止我把這個(gè)想法變成一個(gè)很酷的應(yīng)用程序。

今年夏天,我計(jì)劃帶著我的孩子出國(guó)。

她很興奮。

在此之前,我和妻子決定大肆宣傳一下這次的飛行之旅,主要是為了確保女兒能安穩(wěn)地度過3小時(shí)的飛行時(shí)間。

可能是我們宣傳有點(diǎn)過頭了,以至于當(dāng)我們不得不坐出租車去機(jī)場(chǎng)時(shí),我蹣跚學(xué)步的孩子感到震驚——她原本以為會(huì)從我們家直接走上飛機(jī)。

我們登機(jī)后,發(fā)生了一件令人難以置信的事情。

原來,當(dāng)機(jī)組人員發(fā)現(xiàn)你和一個(gè)癡迷于飛機(jī)的可愛小孩在一起時(shí),他們會(huì)邀請(qǐng)你們?nèi)タ纯瘩{駛艙。

這激發(fā)了我女兒對(duì)飛機(jī)的癡迷。

從那之后,她一直要求我在天上為她尋找飛機(jī),當(dāng)我為她找到一架飛機(jī)時(shí),她很高興。

上周,我們?cè)诨▓@里待了一個(gè)小時(shí),她坐在我的肩上,看著飛機(jī)一架接一架地在夜空中閃爍。

后來我找到了FlightRadar24,它能顯示覆蓋在地圖上的飛機(jī)位置,但美中不足的是,我必須自己調(diào)整方向。

圖片

但是,對(duì)于一個(gè)孩子來說,她可能并不真正理解或關(guān)心地圖是什么。

所以我們有了繼續(xù)解決的新問題,比如方向,比如可用性。

作為一名非物理移動(dòng)技術(shù)主管,我確實(shí)不知道從哪里開始為孩子打造一匹搖馬,但沒有什么能阻止我把這個(gè)想法變成一個(gè)很酷的應(yīng)用程序。

在雷達(dá)上顯示附近的航班

通過研究制定的要求:

  • 該應(yīng)用程序需要保持正確的方向,隨設(shè)備旋轉(zhuǎn),以便顯示飛機(jī)的正確方向。
  • 該應(yīng)用程序必須根據(jù)飛機(jī)的高度將飛機(jī)圖標(biāo)顯示為更大或更小。
  • 該應(yīng)用程序必須很有趣,要有一種復(fù)古兒童玩具的感覺,而不是嚴(yán)肅的商業(yè)應(yīng)用程序。

這些要求導(dǎo)致了一些構(gòu)成概念驗(yàn)證的活動(dòng)部分:

  • 保持方向是差異化產(chǎn)品的核心要求,因?yàn)楝F(xiàn)有解決方案缺少這一點(diǎn)。我不關(guān)心詳細(xì)的航班信息,我只是想制作一個(gè)很酷的雷達(dá)。iOS 核心位置API已被涵蓋,每次用戶重新調(diào)整設(shè)備方向時(shí)都會(huì)提供委托回調(diào)。
  • 最重要的組件是Flight Data API。OpenSky Network正是我所需要的。一個(gè)簡(jiǎn)單的REST API,免費(fèi)供非商業(yè)用途,包含某個(gè)區(qū)域的航班實(shí)時(shí)數(shù)據(jù)。我們希望每隔幾秒就對(duì)這個(gè)端點(diǎn)執(zhí)行操作,以進(jìn)行真實(shí)的雷達(dá)掃描。
  • 為了調(diào)用 API,還需要一些位置數(shù)據(jù)。Core Location可供查詢距用戶位置+/-1度的緯度,精度為0.1度(約10公里),以確保用戶的位置足夠模糊。我們也只需要在每個(gè)會(huì)話中獲取一次該數(shù)據(jù)。
  • 最后,我們需要重新掌握三角學(xué)技能,將飛行位置數(shù)據(jù)與我們自己的定向坐標(biāo)進(jìn)行比較。這將使我們能夠根據(jù)附近的飛機(jī)在天空中與我們的相對(duì)位置,將其繪制到屏幕上的正確位置。

概念驗(yàn)證

對(duì)于圖標(biāo),我選擇了一幅女兒戴著可愛飛行員帽的卡通畫。所以我們已經(jīng)有了應(yīng)用程序名稱:Aviator。

方向

第一個(gè)關(guān)鍵差異化產(chǎn)品要求是保持方向。

為了使用便利,屏幕上的對(duì)象需要與其現(xiàn)實(shí)生活中的位置相對(duì)應(yīng)。因此,當(dāng)用戶旋轉(zhuǎn)時(shí),屏幕本身也會(huì)旋轉(zhuǎn)并保持指向北。

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)
    }
}

同時(shí),為了獲得好看的指南針效果,我還繪制了一組隨旋轉(zhuǎn)角度變化的矩形。

@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)
    }
}

看起來相當(dāng)不錯(cuò),而且也完美地響應(yīng)了我的真實(shí)位置。

圖片

可能你會(huì)注意到一個(gè)有趣的視覺故障,因?yàn)閯?dòng)畫邏輯將0度和360度視為單獨(dú)的數(shù)字——當(dāng)我經(jīng)過正北時(shí),所有矩形都會(huì)旋轉(zhuǎn)。

航班數(shù)據(jù)

熱身結(jié)束,接下來是重要的部分。

OpenSky Network API允許用戶給定一系列緯度和經(jīng)度,通過一個(gè)簡(jiǎn)單的請(qǐng)求返回該范圍內(nèi)的本地航班數(shù)組。這意味著,只需將其粘貼到瀏覽器中,即可找出我可以看到的頭頂上空的航班數(shù)據(jù)。

REST API記錄良好,但數(shù)據(jù)按順序顯示為列表屬性。

圖片

我們需要去解碼它,讓其按順序從JSON響應(yīng)中解析出字段。

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)
        // ...
    }
}

我們還可以編寫一個(gè)簡(jiǎn)單的API,根據(jù)用戶的位置坐標(biāo)執(zhí)行請(qǐng)求。

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)
    }
}

這樣飛行數(shù)據(jù)就被很好地解析為內(nèi)存中對(duì)象的數(shù)組,也變得易于處理。

初步結(jié)果

如何實(shí)際測(cè)試飛機(jī)圖紙的準(zhǔn)確性?

我們可以在這些所有東西下面畫一張地圖:AviatorView頂部的指南針,繪制到屏幕上的飛機(jī),以及樸素的SwiftUI視圖。

@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
    }
}

這是我第一次熬夜跑出來的結(jié)果,與作為事實(shí)來源的FlightRadar進(jìn)行比較。

圖片

可以看到,天空中飛機(jī)的數(shù)量和集群看起來都差不多,但位置卻相差甚遠(yuǎn)。忽然,我靈光一閃,原來還需要使用注釋在地圖上繪制飛機(jī)。

MVP

這個(gè)想法我已經(jīng)醞釀了一整天:我們使用地圖,然后在其精確地理位置的頂部繪制飛機(jī)形狀的注釋,最終,我想找到一種方法來隱藏實(shí)際地圖,并僅將飛機(jī)顯示為雷達(dá)位置上的標(biāo)記。

這應(yīng)該會(huì)給我們帶來我們想要的很酷的、完全定向的雷達(dá)效果。

地圖注釋

在iOS 17中,在地圖上繪制注釋非常簡(jiǎn)單。

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

在這里,出于雷達(dá)的目的,我們希望防止命中測(cè)試——即我不希望地圖是交互式的。在構(gòu)想中,地圖是不可見的,用戶只能看到航班及其位置。

飛機(jī)縮放

定位之后,尺寸調(diào)整是下一個(gè)核心問題,現(xiàn)有的解決方案根本無法很好地處理這個(gè)問題。

我使用飛行高度在地圖注釋中添加了一些簡(jiǎn)單的對(duì)數(shù)縮放,以便更高的飛機(jī)在屏幕上顯得更大。此外,我使用飛機(jī)的真實(shí)屬性,結(jié)合核心位置中的用戶方向,來顯示飛機(jī)面向正確的方向。

@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)
    }
}

用戶調(diào)研

現(xiàn)在是進(jìn)行終極測(cè)試的時(shí)候了。

我和女兒一起去看飛機(jī),現(xiàn)在我們有了真實(shí)的地圖注釋,能在地圖上顯示用戶的位置和方向。最重要的是,它能夠準(zhǔn)確地找到飛機(jī)!

圖片

這獲得了巨大成功,因?yàn)槲覀冊(cè)谶@上面找到了飛機(jī)。

初步測(cè)試還得出了兩條重要信息。

首先,縮放邏輯是不正確的。看看倫敦城市機(jī)場(chǎng)地面上的小飛機(jī)。由于應(yīng)用程序的重點(diǎn)是定位天空中的飛機(jī),因此我們需要反轉(zhuǎn)縮放比例,較低的平面必須顯示得更大,因?yàn)槲覀兪怯醚劬戆l(fā)現(xiàn)它們的。

其次,我的孩子不關(guān)心地圖,只關(guān)心飛機(jī)。如果我想消除噪音并專注于發(fā)現(xiàn)飛機(jī),我需要?jiǎng)h除地圖,并開始建造我的雷達(dá)!

更新縮放邏輯

我輕松地修復(fù)了飛機(jī)的縮放邏輯。

經(jīng)過一番嘗試和錯(cuò)誤后,為了查看屏幕上看起來不錯(cuò)的內(nèi)容,并給出合理的尺寸分布,我選擇了縮放:

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

這些縮放來自我的本地開銷掃描:

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

創(chuàng)建雷達(dá)

我?guī)缀鯗?zhǔn)備好建造我所設(shè)想的雷達(dá)了,但是出現(xiàn)了一個(gè)問題。

API穩(wěn)健性

開源OpenSky API不斷超時(shí),返回502錯(cuò)誤,或者有時(shí)生成帶有空數(shù)據(jù)的200響應(yīng)。

這其實(shí)也不是問題,畢竟這不是個(gè)企業(yè)級(jí)應(yīng)用程序,而且這個(gè)API不需要我花任何費(fèi)用。他們沒有SLA,我也覺得自己沒有資格獲得SLA。不過為了幫助提高客戶端的穩(wěn)健性,我在API調(diào)用中實(shí)現(xiàn)了一些基本的重試邏輯:

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運(yùn)行良好,除了某些高流量時(shí)刻外。

覆蓋地圖

最重要的降噪任務(wù)是使實(shí)際地圖不可見。沒有這個(gè)雷達(dá)就無法工作。

我能夠使用MapPolygon來做到這一點(diǎn),表面上設(shè)計(jì)這樣你就可以放置疊加層來突出顯示地圖的各個(gè)部分。但我想用它來隱藏除注釋之外的所有內(nèi)容。

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)
    }
}

這種方法很有效!

我們現(xiàn)在可以看到飛機(jī),但看不到地圖,就像我們想要的那樣。

最關(guān)鍵的是,蘋果將疊加層設(shè)計(jì)為位于地圖頂部、注釋下方,如果他們采取其他方式,我女兒的新玩具就會(huì)跛行。

繪制雷達(dá)

核心需求的最后一部分是雷達(dá)視圖,這本質(zhì)上是一組直線、同心圓和20度的旋轉(zhuǎn)角梯度。

難不倒我。

用戶調(diào)研2

經(jīng)過三個(gè)晚上的辛苦工作,女兒終于開始對(duì)我創(chuàng)造的玩具表現(xiàn)出一些興趣。

圖片

我們已經(jīng)證明了這個(gè)概念,并構(gòu)建了一個(gè) MVP,可以實(shí)現(xiàn)我們?cè)O(shè)定的核心初始目標(biāo)。

現(xiàn)在可以考慮把它放到App Store上了。

當(dāng)然在此之前還需要進(jìn)行其他的優(yōu)化。

比如讓雷達(dá)有360度寬角漸變,從綠色,到透明,到透明,到透明,再到黑色。

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屏幕效果和電視掃描線,使應(yīng)用程序看起來就像是在舊雷達(dá)掃描儀上繪制的。

#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);
}

我還創(chuàng)建了一個(gè)視圖修改器,可以將CRT效果應(yīng)用到喜歡的任何視圖。

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)
                )
            )
    }
}

圖片

目前該應(yīng)用程序已經(jīng)上線了App Store。


圖片

同時(shí)下個(gè)版本的新功能也已經(jīng)在構(gòu)想中了,包括但不限于:

  • 向地圖添加縮放級(jí)別,以將雷達(dá)限制為僅檢測(cè)較近的飛機(jī)。
  • 使用OpenSky Network API的高級(jí)版本顯示直升機(jī)、衛(wèi)星和飛機(jī)尺寸類別。
  • 切換飛機(jī)上的出發(fā)地和目的地國(guó)家/地區(qū)顯示。
  • 使用更先進(jìn)的金屬著色器改善CRT屏幕效果。
  • 實(shí)施滑塊控件來過濾掉某些距離和高度,例如隱藏所有低矮、遙遠(yuǎn)的飛機(jī)。
  • 實(shí)施“滑稽模式”,在雷達(dá)上呈現(xiàn)不明飛行物、巨型蟲子和外星人。
責(zé)任編輯:姜華 來源: 大數(shù)據(jù)文摘
相關(guān)推薦

2019-05-23 08:55:41

代碼開發(fā)工具

2022-02-22 20:35:22

公鑰私鑰數(shù)據(jù)

2025-03-06 13:10:32

2022-12-05 18:17:06

技術(shù)

2016-12-14 10:00:44

數(shù)據(jù)結(jié)構(gòu)編譯器

2020-07-15 15:09:21

Python掃雷游戲Windows

2020-05-08 13:28:53

新擬物UI設(shè)計(jì)

2018-01-15 15:00:06

工程師項(xiàng)目設(shè)計(jì)師

2022-05-30 08:02:51

事務(wù)日志MySQL數(shù)據(jù)庫(kù)

2020-11-16 09:02:38

Python開發(fā)工具

2021-04-29 15:53:21

AI 數(shù)據(jù)人工智能

2020-06-02 16:38:24

華為

2021-03-02 07:33:10

VSCode插件代碼

2015-05-21 15:46:20

2015-10-15 09:58:26

HRMMMicroservic微服務(wù)

2012-09-11 13:34:27

HTML5JS

2020-04-14 10:32:10

劉強(qiáng)東管理快遞

2019-08-12 10:27:34

前端程序員網(wǎng)絡(luò)

2020-07-27 08:31:45

控制流通用結(jié)構(gòu)

2017-12-15 12:44:02

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)