LAsset Location Asset - kirseia/study GitHub Wiki

import AVFoundation
import CoreLocation
import Photos

class LAsset {
    static let avassetDateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
        return formatter
    }()
    
    static let gpsExifDateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
        return formatter
    }()
    
    // 한 번에 한 개만 요청 가능
    static let geocoder = CLGeocoder()
    
    var location: CLLocation?
    var createdDate: Date?
    var modifiedDate: Date?
    
    var date: Date {
        return createdDate ?? modifiedDate ?? Date(timeIntervalSince1970: 0)
    }
    
    var country: String?
    var city: String?
    var geocodingDone: Bool = false
    
    var isVideo: Bool = false
    
    var item: Any // url, PHAsset, AVAsset
    
    init?(item: Any) {
        self.item = item
        var result = true
        
        switch item {
        case let url as URL:
            result = extractInfo(from: url)
            
        case let asset as AVAsset:
            result = extractInfo(from: asset)
            
        case let phAsset as PHAsset:
            result = extractInfo(from: phAsset)
            
        default:
            return nil
        }
        
        if result == false || (location == nil && createdDate == nil && modifiedDate == nil) {
            return nil
        }
        
        if let location = location {
            LAsset.geocoder.reverseGeocodeLocation(location) { [weak self] (placeMarkList, error) in
                if error == nil {
                    self?.geocodingDone = true
                    return
                }
                
                if let placeMarkList = placeMarkList, placeMarkList.count > 0 {
                    self?.country = placeMarkList[0].country
                    self?.city = placeMarkList[0].locality
                    
                    print(placeMarkList[0])
                }
                
                self?.geocodingDone = true
            }
        }
    }
    
    // MARK: default func
    func distance(from lAsset: LAsset) -> Double {
        guard let aLocation = self.location, let bLocation = lAsset.location else {
            return 0.0
        }
        
        return aLocation.distance(from: bLocation)
    }
    
    func distance(from location: CLLocation?) -> Double {
        guard let aLocation = self.location, let bLocation = location else {
            return 0.0
        }
        
        return aLocation.distance(from: bLocation)
    }
    
    // MARK: private func
    private func extractInfo(from asset: AVAsset) -> Bool {
        let metadataList = asset.metadata
        
        for meta in metadataList {
            guard let key = meta.commonKey else {
                continue
            }
            
            switch key {
            case .commonKeyLocation:
                // "+37.3594+127.1048+061.813/"
                if let locationString = meta.stringValue {
                    location = convert(from: locationString)
                }
            case .commonKeyCreationDate:
                // "2019-01-25T19:49:47+0900"
                createdDate = LAsset.avassetDateFormatter.date(from: meta.stringValue ?? "")
            case .commonKeyLastModifiedDate:
                // "2019-01-25T19:49:47+0900"
                modifiedDate = LAsset.avassetDateFormatter.date(from: meta.stringValue ?? "")
            default:
                continue
            }
        }
        
        isVideo = true
        
        return true
    }
    
    private func extractInfo(from phAsset: PHAsset) -> Bool {
        location = phAsset.location
        createdDate = phAsset.creationDate
        modifiedDate = phAsset.modificationDate
        
        isVideo = phAsset.mediaType == .video
        
        return true
    }
    
    private func extractInfo(from url: URL) -> Bool {
        let videoExtensionList: [String] = ["mov", "mp4", "ts"]
        let pathExtension = url.pathExtension.lowercased()
        
        if videoExtensionList.contains(pathExtension) {
            let asset = AVAsset(url: url)
            return extractInfo(from: asset)
            
        } else {
            return extractImageInfo(from: url)
        }
    }
    
    private func extractImageInfo(from url: URL) -> Bool {
        // 아래는 image 처리
        isVideo = false
        
        guard let data = try? Data(contentsOf: url) else {
            return false
        }
        
        guard let imageData = CGImageSourceCreateWithData(data as CFData, nil) else {
            return false
        }
        
        guard let metaData = CGImageSourceCopyPropertiesAtIndex(imageData, 0, nil) else {
            return false
        }
        
        // TODO: 이전에 실패했을 때 작성 시간 같은걸 따로 처리할까...
        
        let metaDictionary = metaData as NSDictionary
        
        if let exifDictionary = metaDictionary.object(forKey: kCGImagePropertyExifDictionary as String) as? NSDictionary {
            let createdTimeValue = exifDictionary.value(forKey: kCGImagePropertyExifDateTimeOriginal as String)
            
            if let createdTimeString = createdTimeValue as? String {
                // 찍은 위치의 시간, 어디서 찍었는지 gps 가 있으면 UTC 계산이 가능. gps 없으면 불가
                createdDate = LAsset.gpsExifDateFormatter.date(from: createdTimeString)
            }
        }
        
        if let gpsDictionary = metaDictionary.object(forKey: kCGImagePropertyGPSDictionary as String) as? NSDictionary {
            // Ref. https://github.com/devongovett/exif-reader
            // Ref. https://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/GPS.html
            
            let latitudeOpt = gpsDictionary.value(forKey: kCGImagePropertyGPSLatitude as String) as? NSNumber
            let longitudeOpt = gpsDictionary.value(forKey: kCGImagePropertyGPSLongitude as String) as? NSNumber
            let altitudeOpt = gpsDictionary.value(forKey: kCGImagePropertyGPSAltitude as String) as? NSNumber
            
            let speedOpt = gpsDictionary.value(forKey: kCGImagePropertyGPSSpeed as String) as? NSNumber
            
            let dateStampOpt = gpsDictionary.value(forKey: kCGImagePropertyGPSDateStamp as String) as? String
            let timeStampOpt = gpsDictionary.value(forKey: kCGImagePropertyGPSTimeStamp as String) as? String
            
            if createdDate == nil {
                // exif 데이터 없을 때만 확인
                if let dateStamp = dateStampOpt, let timeStamp = timeStampOpt {
                    // UTC 0 시간임, gps latitude / 15 해서 timezone 계산해서 맞춰야 함
                    createdDate = LAsset.gpsExifDateFormatter.date(from: dateStamp + " " + timeStamp)
                }
            }
            
            if let latitude = latitudeOpt, let longitude = longitudeOpt {
                let locationCoordinate = CLLocationCoordinate2D(latitude: latitude.doubleValue, longitude: longitude.doubleValue)
                let defaultDate = Date(timeIntervalSince1970: 0)
                
                location = CLLocation(coordinate: locationCoordinate, altitude: altitudeOpt?.doubleValue ?? 0.0,
                                      horizontalAccuracy: 0.0, verticalAccuracy: 0.0,
                                      course: 0.0, speed: speedOpt?.doubleValue ?? 0.0,
                                      timestamp: createdDate ?? defaultDate)
            }
            
        }
        
        return true
    }
    
    private func convert(from locationString: String) -> CLLocation? {
        if locationString.count < 17 {
            return nil
        }
        
        guard let latitude = Double(locationString.substring(from: 0, to: 8)) else {
            return nil
        }
        
        guard let longitude = Double(locationString.substring(from: 8, to: 17)) else {
            return nil
        }
        
        return CLLocation(latitude: latitude, longitude: longitude)
    }
    
}


extension String {
    func substring(from: Int, to: Int) -> String {
        let start = index(startIndex, offsetBy: from)
        let end = index(start, offsetBy: to - from)
        return String(self[start ..< end])
    }
    
}



// GPS dictionary
/*
 ▿ 15 elements
 ▿ 0 : 2 elements
 - key : ImgDirection
 - value : 58.91798400595128
 ▿ 1 : 2 elements
 - key : LatitudeRef
 - value : N
 ▿ 2 : 2 elements
 - key : HPositioningError
 - value : 65
 ▿ 3 : 2 elements
 - key : DestBearingRef
 - value : M
 ▿ 4 : 2 elements
 - key : Latitude
 - value : 34.84813
 ▿ 5 : 2 elements
 - key : Speed
 - value : 0
 ▿ 6 : 2 elements
 - key : TimeStamp
 - value : 05:43:09 // 현재 타임 + 9 하면 14시 = 오후 2시 43분.
 ▿ 7 : 2 elements
 - key : LongitudeRef
 - value : E
 ▿ 8 : 2 elements
 - key : AltitudeRef
 - value : 0
 ▿ 9 : 2 elements
 - key : Longitude
 - value : 127.34468
 ▿ 10 : 2 elements
 - key : Altitude
 - value : 11.90512752625541
 ▿ 11 : 2 elements
 - key : DateStamp
 - value : 2018:12:15
 ▿ 12 : 2 elements
 - key : DestBearing
 - value : 58.91798400595128
 ▿ 13 : 2 elements
 - key : ImgDirectionRef
 - value : M
 ▿ 14 : 2 elements
 - key : SpeedRef
 - value : K
 */


// TIFF dictionary
/*
 ▿ 13 elements
 ▿ 0 : 2 elements
 - key : {TIFF}
 ▿ value : 10 elements
 ▿ 0 : 2 elements
 - key : ResolutionUnit
 - value : 2
 ▿ 1 : 2 elements
 - key : Software
 - value : 12.1
 ▿ 2 : 2 elements
 - key : TileLength
 - value : 512
 ▿ 3 : 2 elements
 - key : DateTime
 - value : 2018:12:15 14:43:15
 ▿ 4 : 2 elements
 - key : XResolution
 - value : 72
 ▿ 5 : 2 elements
 - key : TileWidth
 - value : 512
 ▿ 6 : 2 elements
 - key : Orientation
 - value : 3
 ▿ 7 : 2 elements
 - key : YResolution
 - value : 72
 ▿ 8 : 2 elements
 - key : Model
 - value : iPhone 8
 ▿ 9 : 2 elements
 - key : Make
 - value : Apple
 ▿ 1 : 2 elements
 - key : Orientation
 - value : 3
 ▿ 2 : 2 elements
 - key : PixelWidth
 - value : 4032
 ▿ 3 : 2 elements
 - key : PixelHeight
 - value : 3024
 ▿ 4 : 2 elements
 - key : {Exif}
 ▿ value : 31 elements
 ▿ 0 : 2 elements
 - key : DateTimeOriginal
 - value : 2018:12:15 14:43:15
 ▿ 1 : 2 elements
 - key : MeteringMode
 - value : 5
 ▿ 2 : 2 elements
 - key : ComponentsConfiguration
 ▿ value : 4 elements
 - 0 : 1
 - 1 : 2
 - 2 : 3
 - 3 : 0
 ▿ 3 : 2 elements
 - key : BrightnessValue
 - value : 3.201931423611111
 ▿ 4 : 2 elements
 - key : FocalLenIn35mmFilm
 - value : 28
 ▿ 5 : 2 elements
 - key : LensMake
 - value : Apple
 ▿ 6 : 2 elements
 - key : FNumber
 - value : 1.8
 ▿ 7 : 2 elements
 - key : FocalLength
 - value : 3.99
 ▿ 8 : 2 elements
 - key : ShutterSpeedValue
 - value : 4.58536651192918
 ▿ 9 : 2 elements
 - key : SubjectArea
 ▿ value : 4 elements
 - 0 : 2015
 - 1 : 1511
 - 2 : 2217
 - 3 : 1330
 ▿ 10 : 2 elements
 - key : ApertureValue
 - value : 1.69599381283836
 ▿ 11 : 2 elements
 - key : SceneType
 - value : 1
 ▿ 12 : 2 elements
 - key : SceneCaptureType
 - value : 0
 ▿ 13 : 2 elements
 - key : ColorSpace
 - value : 65535
 ▿ 14 : 2 elements
 - key : LensSpecification
 ▿ value : 4 elements
 - 0 : 3.99
 - 1 : 3.99
 - 2 : 1.8
 - 3 : 1.8
 ▿ 15 : 2 elements
 - key : PixelYDimension
 - value : 3024
 ▿ 16 : 2 elements
 - key : WhiteBalance
 - value : 0
 ▿ 17 : 2 elements
 - key : FlashPixVersion
 ▿ value : 2 elements
 - 0 : 1
 - 1 : 0
 ▿ 18 : 2 elements
 - key : DateTimeDigitized
 - value : 2018:12:15 14:43:15
 ▿ 19 : 2 elements
 - key : ISOSpeedRatings
 ▿ value : 1 element
 - 0 : 32
 ▿ 20 : 2 elements
 - key : ExposureMode
 - value : 0
 ▿ 21 : 2 elements
 - key : ExifVersion
 ▿ value : 3 elements
 - 0 : 2
 - 1 : 2
 - 2 : 1
 ▿ 22 : 2 elements
 - key : PixelXDimension
 - value : 4032
 ▿ 23 : 2 elements
 - key : LensModel
 - value : iPhone 8 back camera 3.99mm f/1.8
 ▿ 24 : 2 elements
 - key : ExposureProgram
 - value : 2
 ▿ 25 : 2 elements
 - key : ExposureTime
 - value : 0.04166666666666666
 ▿ 26 : 2 elements
 - key : SubsecTimeDigitized
 - value : 632
 ▿ 27 : 2 elements
 - key : Flash
 - value : 16
 ▿ 28 : 2 elements
 - key : SubsecTimeOriginal
 - value : 632
 ▿ 29 : 2 elements
 - key : SensingMethod
 - value : 2
 ▿ 30 : 2 elements
 - key : ExposureBiasValue
 - value : 0
 ▿ 5 : 2 elements
 - key : {GPS}
 ▿ value : 15 elements
 ▿ 0 : 2 elements
 - key : ImgDirection
 - value : 58.91798400595128
 ▿ 1 : 2 elements
 - key : LatitudeRef
 - value : N
 ▿ 2 : 2 elements
 - key : HPositioningError
 - value : 65
 ▿ 3 : 2 elements
 - key : DestBearingRef
 - value : M
 ▿ 4 : 2 elements
 - key : Latitude
 - value : 34.84813
 ▿ 5 : 2 elements
 - key : Speed
 - value : 0
 ▿ 6 : 2 elements
 - key : TimeStamp
 - value : 05:43:09
 ▿ 7 : 2 elements
 - key : LongitudeRef
 - value : E
 ▿ 8 : 2 elements
 - key : AltitudeRef
 - value : 0
 ▿ 9 : 2 elements
 - key : Longitude
 - value : 127.34468
 ▿ 10 : 2 elements
 - key : Altitude
 - value : 11.90512752625541
 ▿ 11 : 2 elements
 - key : DateStamp
 - value : 2018:12:15
 ▿ 12 : 2 elements
 - key : DestBearing
 - value : 58.91798400595128
 ▿ 13 : 2 elements
 - key : ImgDirectionRef
 - value : M
 ▿ 14 : 2 elements
 - key : SpeedRef
 - value : K
 ▿ 6 : 2 elements
 - key : PrimaryImage
 - value : 1
 ▿ 7 : 2 elements
 - key : ProfileName
 - value : Display P3
 ▿ 8 : 2 elements
 - key : DPIWidth
 - value : 72
 ▿ 9 : 2 elements
 - key : DPIHeight
 - value : 72
 ▿ 10 : 2 elements
 - key : ColorModel
 - value : RGB
 ▿ 11 : 2 elements
 - key : {MakerApple}
 ▿ value : 22 elements
 ▿ 0 : 2 elements
 - key : 25
 - value : 0
 ▿ 1 : 2 elements
 - key : 33
 - value : 0
 ▿ 2 : 2 elements
 - key : 26
 - value : q825s
 ▿ 3 : 2 elements
 - key : 12
 ▿ value : 2 elements
 - 0 : 2.460938
 - 1 : 5.484375
 ▿ 4 : 2 elements
 - key : 1
 - value : 10
 ▿ 5 : 2 elements
 - key : 20
 - value : 5
 ▿ 6 : 2 elements
 - key : 2
 - value : <9d01db00 05016501 73007200 9500c400 61009000 d0009300 3f003700 3d008f00 4a01f300 1301b800 4b003900 1e008500 8800b300 82007600 40003b00 54007300 30016e01 d900e000 fe004d00 18006600 6700bb00 4a017b00 46001900 34007a00 8801d301 d8004a01 a700ff00 52005300 ad002c01 de01f100 2f003800 7b007c00 ce00ed01 7c01e100 ac004e01 7e004900 79003101 c701ff00 4b004900 d1008600 98005b01 bd00b600 c400eb00 99007800 8e00a700 4c01dc00 a7006000 b7008a00 5500fd00 1a01fd00 d400d500 dc005e00 48005300 77004100 5500cf00 f600ab00 34007800 4f01f100 2401d100 e4004d00 9c006100 3b004600 45009500 6c00ef00 46007c00 f4001701 5801a600 dc005300 77005e00 29003900 4600bc00 e5009e01 4e01f200 ce008601 e400ca00 dc005300 6f004b00 3c002e00 8700b600 42010401 43013b01 9e017500 3800a500 d8007b00 42004400 9100f400 d8000901 fc007100 dc007801 e501e400 46001801 97001701 5201da00 1f01d400 c1002600 e0002a01 fb007001 a501be00 9800df00 8200aa01 f0001a01 03010901 af00d400 a400a700 ac006200 68008600 a600ed00 ae00e200 fd00b300 06012c01 7f009100 9a009000 6f003400 5e008d00 8f009000 8b007900 93006a00 64004e00 82009600 a1002201 5b003d00 6f006a00 6a010e01 72006200 67004d00 1e002c00 6b008700 80005d01>
 ▿ 7 : 2 elements
 - key : 13
 - value : 5
 ▿ 8 : 2 elements
 - key : 3
 ▿ value : 4 elements
 ▿ 0 : 2 elements
 - key : flags
 - value : 1
 ▿ 1 : 2 elements
 - key : value
 - value : 199027025838083
 ▿ 2 : 2 elements
 - key : timescale
 - value : 1000000000
 ▿ 3 : 2 elements
 - key : epoch
 - value : 0
 ▿ 9 : 2 elements
 - key : 14
 - value : 0
 ▿ 10 : 2 elements
 - key : 4
 - value : 1
 ▿ 11 : 2 elements
 - key : 37
 - value : 0
 ▿ 12 : 2 elements
 - key : 5
 - value : 180
 ▿ 13 : 2 elements
 - key : 15
 - value : 2
 ▿ 14 : 2 elements
 - key : 6
 - value : 172
 ▿ 15 : 2 elements
 - key : 38
 - value : 0
 ▿ 16 : 2 elements
 - key : 23
 - value : 8192
 ▿ 17 : 2 elements
 - key : 16
 - value : 1
 ▿ 18 : 2 elements
 - key : 7
 - value : 1
 ▿ 19 : 2 elements
 - key : 39
 - value : 0
 ▿ 20 : 2 elements
 - key : 31
 - value : 0
 ▿ 21 : 2 elements
 - key : 8
 ▿ value : 3 elements
 - 0 : 0.09030848
 - 1 : 0.009026065
 - 2 : -1.017984
 ▿ 12 : 2 elements
 - key : Depth
 - value : 8
 */